mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-24 17:15:50 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3121f0dfb | ||
|
|
8f6d4f88dd |
21
packages/workflow-engine/LICENSE
Normal file
21
packages/workflow-engine/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 claude-code-best
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
71
packages/workflow-engine/README.md
Normal file
71
packages/workflow-engine/README.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# @claude-code-best/workflow-engine
|
||||
|
||||
Deterministic JS script orchestration engine for multi-agent workflows. The core layer has zero runtime dependencies and talks to the outside world exclusively through **port adapters** — you bring your own agent backend, journal store, and progress sink.
|
||||
|
||||
## Why
|
||||
|
||||
When you orchestrate multiple LLM agents, you want the orchestration itself to be **deterministic, replayable, and testable**. This engine runs a plain JS script (compiled by Bun's transpiler) with primitives like `agent()`, `phase()`, `parallel()` and `pipeline()`. The non-deterministic parts (the LLM, the file system, the clock) are isolated behind ports, so the same script produces the same journal on every replay.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @claude-code-best/workflow-engine
|
||||
# or
|
||||
npm install @claude-code-best/workflow-engine
|
||||
```
|
||||
|
||||
Runtime peer requirements: `ajv` and `zod` are pulled in automatically as dependencies.
|
||||
|
||||
## Minimal example
|
||||
|
||||
```ts
|
||||
import {
|
||||
createFileJournalStore,
|
||||
createHostHandle,
|
||||
runWorkflow,
|
||||
type WorkflowPorts,
|
||||
} from '@claude-code-best/workflow-engine'
|
||||
|
||||
const script = `
|
||||
export const meta = { name: 'hello', description: 'minimal demo' }
|
||||
phase('Greet')
|
||||
const reply = await agent({ prompt: 'Say hi in one short sentence.' })
|
||||
emit('result', { reply })
|
||||
`
|
||||
|
||||
const ports: WorkflowPorts = {
|
||||
// Provide your own agent runner + journal + progress emitter.
|
||||
// See examples/smoke.ts for a complete Anthropic SDK wiring.
|
||||
} as WorkflowPorts
|
||||
|
||||
const handle = createHostHandle()
|
||||
await runWorkflow({
|
||||
script,
|
||||
ports,
|
||||
workflowDir: '.wfe/runs/hello',
|
||||
hostHandle: handle,
|
||||
})
|
||||
```
|
||||
|
||||
For a fully wired end-to-end example with the Anthropic SDK, see [`examples/smoke.ts`](./examples/smoke.ts).
|
||||
|
||||
## Core primitives
|
||||
|
||||
- `agent(params)` — call the configured AgentRunner; supports structured-output via JSON Schema.
|
||||
- `phase(name)` — declare a logical phase (display + progress grouping).
|
||||
- `parallel([...])` — barrier-style fan-out with bounded concurrency.
|
||||
- `pipeline(stream, fn)` — streaming pipeline with per-item hooks.
|
||||
- `emit(type, payload)` — emit a progress event to the host.
|
||||
- `log.*` / hooks / budgets — see the TypeScript definitions for the full surface.
|
||||
|
||||
## Building from source
|
||||
|
||||
```bash
|
||||
bun install # from the repo root
|
||||
bun run build # outputs dist/index.js + dist/**/*.d.ts
|
||||
bun test # 178 tests
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT © claude-code-best
|
||||
@@ -1,19 +1,69 @@
|
||||
{
|
||||
"name": "@claude-code-best/workflow-engine",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Deterministic JS script orchestration engine for multi-agent workflows. Zero core-layer runtime dependencies; talks to the world via port adapters.",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"license": "MIT",
|
||||
"author": "claude-code-best <claude-code-best@proton.me>",
|
||||
"homepage": "https://github.com/claude-code-best/claude-code/tree/main/packages/workflow-engine#readme",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/claude-code-best/claude-code.git",
|
||||
"directory": "packages/workflow-engine"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/claude-code-best/claude-code/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"workflow",
|
||||
"orchestration",
|
||||
"multi-agent",
|
||||
"claude",
|
||||
"automation",
|
||||
"scripting",
|
||||
"deterministic"
|
||||
],
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"!src/**/__tests__",
|
||||
"!src/**/*.test.ts",
|
||||
"scripts/build.ts",
|
||||
"tsconfig.json",
|
||||
"tsconfig.build.json",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "^8.18.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "^0.81.0"
|
||||
"@anthropic-ai/sdk": "^0.81.0",
|
||||
"bun-types": "latest"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "bun run scripts/build.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "bun test",
|
||||
"prepublishOnly": "bun run test && bun run build"
|
||||
}
|
||||
}
|
||||
|
||||
21
packages/workflow-engine/scripts/build.ts
Normal file
21
packages/workflow-engine/scripts/build.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { mkdir, rm } from 'node:fs/promises'
|
||||
|
||||
const ROOT = new URL('../', import.meta.url)
|
||||
const DIST = new URL('../dist/', import.meta.url)
|
||||
|
||||
await rm(DIST, { recursive: true, force: true })
|
||||
await mkdir(DIST, { recursive: true })
|
||||
|
||||
// Emit dist/**/*.js + dist/**/*.d.ts (+ maps) via tsc.
|
||||
const proc = Bun.spawn(['bunx', 'tsc', '-p', 'tsconfig.build.json'], {
|
||||
cwd: ROOT.pathname,
|
||||
stdout: 'inherit',
|
||||
stderr: 'inherit',
|
||||
})
|
||||
const exitCode = await proc.exited
|
||||
if (exitCode !== 0) {
|
||||
console.error('tsc emit failed')
|
||||
process.exit(exitCode)
|
||||
}
|
||||
|
||||
console.log('✓ build complete')
|
||||
@@ -16,10 +16,16 @@ export * from './engine/context.js'
|
||||
export * from './engine/hooks.js'
|
||||
export * from './engine/runWorkflow.js'
|
||||
export * from './progress/events.js'
|
||||
export {
|
||||
import {
|
||||
createWorkflowTool,
|
||||
type WorkflowToolDescriptor,
|
||||
} from './tool/WorkflowTool.js'
|
||||
export { workflowInputSchema, type WorkflowInput } from './tool/schema.js'
|
||||
export { persistInlineScript } from './tool/persistInline.js'
|
||||
export { WORKFLOW_TOOL_NAME } from './tool/constants.js'
|
||||
import { workflowInputSchema, type WorkflowInput } from './tool/schema.js'
|
||||
import { persistInlineScript } from './tool/persistInline.js'
|
||||
export {
|
||||
createWorkflowTool,
|
||||
type WorkflowToolDescriptor,
|
||||
workflowInputSchema,
|
||||
type WorkflowInput,
|
||||
persistInlineScript,
|
||||
}
|
||||
|
||||
21
packages/workflow-engine/tsconfig.build.json
Normal file
21
packages/workflow-engine/tsconfig.build.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"declarationMap": false,
|
||||
"sourceMap": false,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"target": "ESNext"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"src/**/__tests__/**",
|
||||
"examples/**",
|
||||
"scripts/**"
|
||||
]
|
||||
}
|
||||
18
src/setup.ts
18
src/setup.ts
@@ -94,10 +94,20 @@ export async function setup(
|
||||
// (SessionStart in particular) can spawn and snapshot process.env.
|
||||
if (feature('UDS_INBOX')) {
|
||||
const m = await import('./utils/udsMessaging.js')
|
||||
await m.startUdsMessaging(
|
||||
messagingSocketPath ?? m.getDefaultUdsSocketPath(),
|
||||
{ isExplicit: messagingSocketPath !== undefined },
|
||||
)
|
||||
try {
|
||||
await m.startUdsMessaging(
|
||||
messagingSocketPath ?? m.getDefaultUdsSocketPath(),
|
||||
{ isExplicit: messagingSocketPath !== undefined },
|
||||
)
|
||||
} catch (error) {
|
||||
logError(error)
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Error: Failed to start messaging socket (UDS_INBOX): ${errorMessage(error)}`,
|
||||
),
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
MAX_UDS_INBOX_BYTES,
|
||||
MAX_UDS_FRAME_BYTES,
|
||||
MAX_UDS_CLIENTS,
|
||||
MAX_UNIX_SOCKET_PATH_LENGTH,
|
||||
assertValidUnixSocketPath,
|
||||
formatUdsAddress,
|
||||
parseUdsTarget,
|
||||
sendUdsMessage,
|
||||
@@ -34,11 +36,23 @@ let previousConfigDir: string | undefined
|
||||
let tempConfigDir = ''
|
||||
|
||||
function socketPath(label: string): string {
|
||||
const suffix = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}-${label}`
|
||||
const suffix = `${process.pid}-${Math.random().toString(16).slice(2)}-${label}`
|
||||
if (process.platform === 'win32') {
|
||||
return `\\\\.\\pipe\\claude-code-test-${suffix}`
|
||||
}
|
||||
return join(tmpdir(), 'claude-code-test', `${suffix}.sock`)
|
||||
const base =
|
||||
process.platform === 'darwin'
|
||||
? '/tmp/claude-uds-test'
|
||||
: join(tmpdir(), 'cc-uds-test')
|
||||
return join(base, `${suffix}.sock`)
|
||||
}
|
||||
|
||||
function shortTestDir(prefix: string): string {
|
||||
const id = `${process.pid}-${Math.random().toString(16).slice(2)}`
|
||||
if (process.platform === 'darwin') {
|
||||
return join('/tmp', `${prefix}-${id}`)
|
||||
}
|
||||
return join(tmpdir(), `${prefix}-${id}`)
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
@@ -499,6 +513,27 @@ describe('UDS inbox retention', () => {
|
||||
expect(getDefaultUdsSocketPath()).not.toBe(firstPath)
|
||||
})
|
||||
|
||||
test('default socket path stays within AF_UNIX length limit', () => {
|
||||
const path = getDefaultUdsSocketPath()
|
||||
if (process.platform === 'win32') return
|
||||
expect(Buffer.byteLength(path, 'utf8')).toBeLessThanOrEqual(
|
||||
MAX_UNIX_SOCKET_PATH_LENGTH,
|
||||
)
|
||||
expect(() => assertValidUnixSocketPath(path)).not.toThrow()
|
||||
})
|
||||
|
||||
test('rejects socket paths longer than AF_UNIX limit', () => {
|
||||
if (process.platform === 'win32') return
|
||||
const longPath = `/tmp/${'x'.repeat(MAX_UNIX_SOCKET_PATH_LENGTH)}.sock`
|
||||
expect(() => assertValidUnixSocketPath(longPath)).toThrow(/max 104/)
|
||||
})
|
||||
|
||||
test('default socket path can bind on Node.js', async () => {
|
||||
const path = getDefaultUdsSocketPath()
|
||||
await startUdsMessaging(path, { isExplicit: true })
|
||||
await stopUdsMessaging()
|
||||
})
|
||||
|
||||
test('rejects oversized receiver responses before retaining them', async () => {
|
||||
const path = socketPath('oversized-response')
|
||||
if (process.platform !== 'win32') {
|
||||
@@ -688,10 +723,7 @@ describe('UDS inbox retention', () => {
|
||||
})
|
||||
|
||||
test('fails closed when an explicit socket parent is not private', async () => {
|
||||
const parent = join(
|
||||
tmpdir(),
|
||||
`uds-socket-parent-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||
)
|
||||
const parent = shortTestDir('uds-sp')
|
||||
await mkdir(parent, { recursive: true, mode: 0o755 })
|
||||
await chmod(parent, 0o755)
|
||||
|
||||
@@ -707,10 +739,7 @@ describe('UDS inbox retention', () => {
|
||||
})
|
||||
|
||||
test('fails closed when an explicit socket parent is a file', async () => {
|
||||
const parentFile = join(
|
||||
tmpdir(),
|
||||
`uds-socket-parent-file-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||
)
|
||||
const parentFile = shortTestDir('uds-spf')
|
||||
await writeFile(parentFile, 'not a directory', 'utf-8')
|
||||
|
||||
try {
|
||||
|
||||
@@ -85,13 +85,26 @@ export const MAX_UDS_CLIENTS = 128
|
||||
export const UDS_AUTH_TIMEOUT_MS = 2_000
|
||||
export const UDS_IDLE_TIMEOUT_MS = 30_000
|
||||
|
||||
/** macOS/BSD AF_UNIX `sun_path` limit (bytes, excluding NUL). */
|
||||
export const MAX_UNIX_SOCKET_PATH_LENGTH = 104
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API — socket path helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function assertValidUnixSocketPath(path: string): void {
|
||||
if (process.platform === 'win32') return
|
||||
const byteLength = Buffer.byteLength(path, 'utf8')
|
||||
if (byteLength > MAX_UNIX_SOCKET_PATH_LENGTH) {
|
||||
throw new Error(
|
||||
`[udsMessaging] socket path is ${byteLength} bytes (max ${MAX_UNIX_SOCKET_PATH_LENGTH}): ${path}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default socket path based on PID, placed in a tmpdir subdirectory so it
|
||||
* survives across config-home changes and avoids polluting ~/.claude.
|
||||
* Default socket path based on PID. Uses a flat file under a short temp
|
||||
* directory so the path stays within the AF_UNIX limit on macOS.
|
||||
*
|
||||
* On Windows, Node.js requires named pipe paths in the `\\.\pipe\` namespace;
|
||||
* file-system paths like `C:\...\Temp\x.sock` cause EACCES. Bun handles both
|
||||
@@ -99,17 +112,19 @@ export const UDS_IDLE_TIMEOUT_MS = 30_000
|
||||
*/
|
||||
export function getDefaultUdsSocketPath(): string {
|
||||
if (defaultSocketPath) return defaultSocketPath
|
||||
const nonce = randomBytes(16).toString('hex')
|
||||
const nonce = randomBytes(8).toString('hex')
|
||||
if (process.platform === 'win32') {
|
||||
defaultSocketPath = `\\\\.\\pipe\\claude-code-${process.pid}-${nonce}`
|
||||
return defaultSocketPath
|
||||
}
|
||||
|
||||
defaultSocketPath = join(
|
||||
tmpdir(),
|
||||
'claude-code-socks',
|
||||
'cc-socks',
|
||||
`${process.pid}-${nonce}`,
|
||||
'messaging.sock',
|
||||
)
|
||||
assertValidUnixSocketPath(defaultSocketPath)
|
||||
return defaultSocketPath
|
||||
}
|
||||
|
||||
@@ -416,6 +431,8 @@ export async function startUdsMessaging(
|
||||
return
|
||||
}
|
||||
|
||||
assertValidUnixSocketPath(path)
|
||||
|
||||
// Ensure parent directory exists (skip on Windows — pipe paths aren't files)
|
||||
if (process.platform !== 'win32') {
|
||||
await ensureSocketParent(path)
|
||||
|
||||
Reference in New Issue
Block a user