Compare commits

...

2 Commits
v2.8.1 ... main

Author SHA1 Message Date
xiaoFjun-eng
d3121f0dfb Fix type (#1274)
* 完善所有用到的type对象,并添加中文注释

* 补充遗失的type

* 修改pipe模式卡死的问题,增加trycatch捕获错误信息。

* 修改为英文报错信息。
2026-06-23 19:47:09 +08:00
claude-code-best
8f6d4f88dd chore(workflow-engine): 封包发布到 npm
- 移除 private,补全 exports/types/files/publishConfig/license/repository 等
- 添加 LICENSE (MIT) 与 README
- 添加 scripts/build.ts + tsconfig.build.json,用 tsc emit 输出 dist/**/*.js + .d.ts
  (Bun bundle + external zod 会丢失 createWorkflowTool/workflowInputSchema/persistInlineScript 符号,改用 tsc emit)
- 修 src/index.ts 的 WORKFLOW_TOOL_NAME 重复 export;tool/* 的 named re-export 改为 import + 再 export

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
2026-06-22 20:09:24 +08:00
9 changed files with 273 additions and 27 deletions

View 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.

View 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

View File

@@ -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"
}
}

View 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')

View File

@@ -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,
}

View 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/**"
]
}

View File

@@ -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')
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)
}
}
}

View File

@@ -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 {

View File

@@ -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)