chore: remove deprecated json-cas-workflow package
Types moved to @uncaged/workflow-protocol. npm package deprecated.
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@
|
||||
"typescript": "^5.8.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --build packages/json-cas packages/json-cas-fs packages/json-cas-workflow",
|
||||
"build": "tsc --build packages/json-cas packages/json-cas-fs",
|
||||
"test": "bun test",
|
||||
"check": "biome check .",
|
||||
"format": "biome format --write .",
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# @uncaged/json-cas-workflow
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @uncaged/json-cas@0.3.0
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @uncaged/json-cas@0.2.0
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: replace workspace:^ with actual version numbers in published dependencies
|
||||
|
||||
- Updated dependencies []:
|
||||
- @uncaged/json-cas@0.1.3
|
||||
@@ -1,177 +0,0 @@
|
||||
# @uncaged/json-cas-workflow
|
||||
|
||||
Workflow integration layer (schemas + types).
|
||||
|
||||
## Overview
|
||||
|
||||
`@uncaged/json-cas-workflow` registers eleven JSON Schemas for agent/workflow execution graphs (definitions, thread lifecycle, content, and React-style tool/session nodes) and exports matching TypeScript payload types. Call `registerWorkflowSchemas(store)` once per store to obtain type hashes for each schema.
|
||||
|
||||
Sits above `@uncaged/json-cas`; typically used with `json-cas-fs` or `createMemoryStore` when building workflow-aware applications.
|
||||
|
||||
**Dependencies:** `@uncaged/json-cas`
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @uncaged/json-cas-workflow
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
Exported from `src/index.ts`.
|
||||
|
||||
### Schema registry
|
||||
|
||||
```typescript
|
||||
type WorkflowSchemaHashes = {
|
||||
agent: Hash;
|
||||
roleSchema: Hash;
|
||||
role: Hash;
|
||||
workflow: Hash;
|
||||
threadStart: Hash;
|
||||
threadStep: Hash;
|
||||
threadEnd: Hash;
|
||||
content: Hash;
|
||||
reactSession: Hash;
|
||||
reactTurn: Hash;
|
||||
reactToolCall: Hash;
|
||||
};
|
||||
|
||||
async function registerWorkflowSchemas(store: Store): Promise<WorkflowSchemaHashes>;
|
||||
```
|
||||
|
||||
Idempotent: safe to call multiple times on the same store (duplicate puts return the same hashes).
|
||||
|
||||
### Payload types
|
||||
|
||||
Definition layer:
|
||||
|
||||
```typescript
|
||||
type AgentPayload = {
|
||||
package: string;
|
||||
version: string;
|
||||
config: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type RoleSchemaPayload = Record<string, unknown>;
|
||||
|
||||
type RolePayload = {
|
||||
name: string;
|
||||
description: string;
|
||||
systemPrompt: string;
|
||||
extractPrompt: string;
|
||||
schema: Hash; // cas_ref → role-schema
|
||||
};
|
||||
|
||||
type WorkflowTransition = {
|
||||
from: string;
|
||||
to: string;
|
||||
when: string | null;
|
||||
};
|
||||
|
||||
type WorkflowPayload = {
|
||||
name: string;
|
||||
description: string;
|
||||
roles: Record<string, Hash>; // cas_ref → role
|
||||
moderator: WorkflowTransition[];
|
||||
};
|
||||
```
|
||||
|
||||
Execution layer:
|
||||
|
||||
```typescript
|
||||
type ThreadStartPayload = {
|
||||
workflow: Hash;
|
||||
input: string;
|
||||
depth: number;
|
||||
parentThread: Hash | null;
|
||||
agents: Record<string, Hash>;
|
||||
};
|
||||
|
||||
type ThreadStepPayload = {
|
||||
role: string;
|
||||
meta: Record<string, unknown>;
|
||||
content: Hash;
|
||||
react: Hash;
|
||||
start: Hash;
|
||||
previous: Hash | null;
|
||||
};
|
||||
|
||||
type ThreadEndPayload = {
|
||||
returnCode: number;
|
||||
summary: string;
|
||||
start: Hash;
|
||||
lastStep: Hash;
|
||||
};
|
||||
|
||||
type ContentPayload = {
|
||||
text: string;
|
||||
};
|
||||
```
|
||||
|
||||
React layer:
|
||||
|
||||
```typescript
|
||||
type ReactTurnTokens = {
|
||||
input: number;
|
||||
output: number;
|
||||
};
|
||||
|
||||
type ReactSessionPayload = {
|
||||
agent: Hash;
|
||||
role: string;
|
||||
turns: Hash[];
|
||||
totalTokens: number;
|
||||
durationMs: number;
|
||||
};
|
||||
|
||||
type ReactTurnPayload = {
|
||||
input: Hash;
|
||||
output: Hash;
|
||||
toolCalls: Hash[];
|
||||
tokens: ReactTurnTokens;
|
||||
latencyMs: number;
|
||||
};
|
||||
|
||||
type ReactToolCallPayload = {
|
||||
name: string;
|
||||
arguments: Hash;
|
||||
result: Hash;
|
||||
durationMs: number;
|
||||
};
|
||||
```
|
||||
|
||||
(`Hash` is imported from `@uncaged/json-cas` in source; consumers should import `Hash` from `@uncaged/json-cas` when typing their own code.)
|
||||
|
||||
### Example
|
||||
|
||||
```typescript
|
||||
import { bootstrap, createMemoryStore } from "@uncaged/json-cas";
|
||||
import {
|
||||
registerWorkflowSchemas,
|
||||
type WorkflowPayload,
|
||||
} from "@uncaged/json-cas-workflow";
|
||||
|
||||
const store = createMemoryStore();
|
||||
await bootstrap(store);
|
||||
|
||||
const schemas = await registerWorkflowSchemas(store);
|
||||
|
||||
const workflowHash = await store.put(schemas.workflow, {
|
||||
name: "demo",
|
||||
description: "Example workflow",
|
||||
roles: {},
|
||||
moderator: [],
|
||||
} satisfies WorkflowPayload);
|
||||
|
||||
console.log(workflowHash);
|
||||
```
|
||||
|
||||
## Internal Structure
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `schemas.ts` | JSON Schema definitions and `registerWorkflowSchemas` |
|
||||
| `types.ts` | Payload TypeScript types |
|
||||
| `index.ts` | Public exports |
|
||||
| `index.test.ts` | Registry and schema tests |
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"name": "@uncaged/json-cas-workflow",
|
||||
"version": "0.5.2",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "bun test",
|
||||
"prepublishOnly": "echo '请用 bun run release 从根目录发版' && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/json-cas": "^0.5.0"
|
||||
}
|
||||
}
|
||||
@@ -1,646 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { CasNode } from "@uncaged/json-cas";
|
||||
import {
|
||||
createMemoryStore,
|
||||
getSchema,
|
||||
refs,
|
||||
validate,
|
||||
walk,
|
||||
} from "@uncaged/json-cas";
|
||||
import type { WorkflowSchemaHashes } from "./schemas.js";
|
||||
import { registerWorkflowSchemas } from "./schemas.js";
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Step 1: registerWorkflowSchemas() — registers all 11 schemas
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
describe("registerWorkflowSchemas", () => {
|
||||
test("returns an object with all 11 schema hashes", async () => {
|
||||
const store = createMemoryStore();
|
||||
const hashes = await registerWorkflowSchemas(store);
|
||||
|
||||
const keys: (keyof WorkflowSchemaHashes)[] = [
|
||||
"agent",
|
||||
"roleSchema",
|
||||
"role",
|
||||
"workflow",
|
||||
"threadStart",
|
||||
"threadStep",
|
||||
"threadEnd",
|
||||
"content",
|
||||
"reactSession",
|
||||
"reactTurn",
|
||||
"reactToolCall",
|
||||
];
|
||||
expect(Object.keys(hashes)).toHaveLength(11);
|
||||
for (const key of keys) {
|
||||
expect(hashes[key]).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
test("all hashes are valid 13-char Crockford Base32 strings", async () => {
|
||||
const store = createMemoryStore();
|
||||
const hashes = await registerWorkflowSchemas(store);
|
||||
|
||||
for (const hash of Object.values(hashes)) {
|
||||
expect(hash).toHaveLength(13);
|
||||
expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||
}
|
||||
});
|
||||
|
||||
test("all 11 hashes are distinct", async () => {
|
||||
const store = createMemoryStore();
|
||||
const hashes = await registerWorkflowSchemas(store);
|
||||
|
||||
const values = Object.values(hashes);
|
||||
const unique = new Set(values);
|
||||
expect(unique.size).toBe(11);
|
||||
});
|
||||
|
||||
test("is idempotent: repeated calls return the same hashes", async () => {
|
||||
const store = createMemoryStore();
|
||||
const first = await registerWorkflowSchemas(store);
|
||||
const second = await registerWorkflowSchemas(store);
|
||||
|
||||
for (const key of Object.keys(first) as (keyof WorkflowSchemaHashes)[]) {
|
||||
expect(first[key]).toBe(second[key]);
|
||||
}
|
||||
});
|
||||
|
||||
test("schemas are stored in the store (getSchema returns non-null)", async () => {
|
||||
const store = createMemoryStore();
|
||||
const hashes = await registerWorkflowSchemas(store);
|
||||
|
||||
for (const hash of Object.values(hashes)) {
|
||||
expect(getSchema(store, hash)).not.toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Step 2: getSchema() — schema round-trip for each of the 11 types
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
describe("getSchema round-trip", () => {
|
||||
test("agent schema has the expected properties", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { agent } = await registerWorkflowSchemas(store);
|
||||
const schema = getSchema(store, agent);
|
||||
|
||||
expect(schema).not.toBeNull();
|
||||
expect(schema?.type).toBe("object");
|
||||
const props = schema?.properties as Record<string, unknown>;
|
||||
expect(props).toHaveProperty("package");
|
||||
expect(props).toHaveProperty("version");
|
||||
expect(props).toHaveProperty("config");
|
||||
});
|
||||
|
||||
test("role schema references cas_ref for the schema field", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { role } = await registerWorkflowSchemas(store);
|
||||
const schema = getSchema(store, role);
|
||||
|
||||
expect(schema).not.toBeNull();
|
||||
const props = schema?.properties as Record<string, { format?: string }>;
|
||||
expect(props.schema?.format).toBe("cas_ref");
|
||||
});
|
||||
|
||||
test("thread-step schema has six required fields", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { threadStep } = await registerWorkflowSchemas(store);
|
||||
const schema = getSchema(store, threadStep);
|
||||
|
||||
expect(schema?.required).toHaveLength(6);
|
||||
});
|
||||
|
||||
test("react-turn schema has nested tokens object", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { reactTurn } = await registerWorkflowSchemas(store);
|
||||
const schema = getSchema(store, reactTurn);
|
||||
|
||||
const props = schema?.properties as Record<
|
||||
string,
|
||||
{ type: string; properties?: unknown }
|
||||
>;
|
||||
expect(props.tokens?.type).toBe("object");
|
||||
expect(props.tokens?.properties).toBeDefined();
|
||||
});
|
||||
|
||||
test("workflow schema has roles with additionalProperties cas_ref", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { workflow } = await registerWorkflowSchemas(store);
|
||||
const schema = getSchema(store, workflow);
|
||||
|
||||
const props = schema?.properties as Record<
|
||||
string,
|
||||
{ additionalProperties?: { format?: string } }
|
||||
>;
|
||||
expect(props.roles?.additionalProperties?.format).toBe("cas_ref");
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Step 3: validate() — correct payloads pass for all 11 schema types
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
describe("validate – valid payloads", () => {
|
||||
const HASH = "AAAAAAAAAAAAA";
|
||||
|
||||
test("agent payload is valid", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { agent } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(agent, {
|
||||
package: "gpt-4o",
|
||||
version: "2024-11",
|
||||
config: { temperature: 0.7 },
|
||||
});
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
||||
});
|
||||
|
||||
test("role-schema payload is valid (any object)", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { roleSchema } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(roleSchema, {
|
||||
type: "object",
|
||||
properties: { answer: { type: "string" } },
|
||||
});
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
||||
});
|
||||
|
||||
test("role payload is valid", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { role } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(role, {
|
||||
name: "analyst",
|
||||
description: "Analyses data",
|
||||
systemPrompt: "You are an analyst.",
|
||||
extractPrompt: "Extract the findings.",
|
||||
schema: HASH,
|
||||
});
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
||||
});
|
||||
|
||||
test("workflow payload is valid", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { workflow } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(workflow, {
|
||||
name: "research",
|
||||
description: "Research workflow",
|
||||
roles: { analyst: HASH },
|
||||
moderator: [{ from: "analyst", to: "analyst", when: null }],
|
||||
});
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
||||
});
|
||||
|
||||
test("thread-start payload is valid (null parentThread)", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { threadStart } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(threadStart, {
|
||||
workflow: HASH,
|
||||
input: "hello",
|
||||
depth: 0,
|
||||
parentThread: null,
|
||||
agents: { main: HASH },
|
||||
});
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
||||
});
|
||||
|
||||
test("thread-start payload is valid (non-null parentThread)", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { threadStart } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(threadStart, {
|
||||
workflow: HASH,
|
||||
input: "nested",
|
||||
depth: 1,
|
||||
parentThread: HASH,
|
||||
agents: {},
|
||||
});
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
||||
});
|
||||
|
||||
test("thread-step payload is valid (null previous)", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { threadStep } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(threadStep, {
|
||||
role: "analyst",
|
||||
meta: { attempt: 1 },
|
||||
content: HASH,
|
||||
react: HASH,
|
||||
start: HASH,
|
||||
previous: null,
|
||||
});
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
||||
});
|
||||
|
||||
test("thread-step payload is valid (non-null previous)", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { threadStep } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(threadStep, {
|
||||
role: "analyst",
|
||||
meta: {},
|
||||
content: HASH,
|
||||
react: HASH,
|
||||
start: HASH,
|
||||
previous: HASH,
|
||||
});
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
||||
});
|
||||
|
||||
test("thread-end payload is valid", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { threadEnd } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(threadEnd, {
|
||||
returnCode: 0,
|
||||
summary: "Done",
|
||||
start: HASH,
|
||||
lastStep: HASH,
|
||||
});
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
||||
});
|
||||
|
||||
test("content payload is valid", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { content } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(content, { text: "Hello, world!" });
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
||||
});
|
||||
|
||||
test("react-session payload is valid (empty turns)", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { reactSession } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(reactSession, {
|
||||
agent: HASH,
|
||||
role: "analyst",
|
||||
turns: [],
|
||||
totalTokens: 0,
|
||||
durationMs: 42,
|
||||
});
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
||||
});
|
||||
|
||||
test("react-session payload is valid (multiple turns)", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { reactSession } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(reactSession, {
|
||||
agent: HASH,
|
||||
role: "analyst",
|
||||
turns: [HASH, HASH],
|
||||
totalTokens: 300,
|
||||
durationMs: 1500,
|
||||
});
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
||||
});
|
||||
|
||||
test("react-turn payload is valid", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { reactTurn } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(reactTurn, {
|
||||
input: HASH,
|
||||
output: HASH,
|
||||
toolCalls: [HASH],
|
||||
tokens: { input: 100, output: 50 },
|
||||
latencyMs: 800,
|
||||
});
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
||||
});
|
||||
|
||||
test("react-tool-call payload is valid", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { reactToolCall } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(reactToolCall, {
|
||||
name: "search",
|
||||
arguments: HASH,
|
||||
result: HASH,
|
||||
durationMs: 200,
|
||||
});
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Step 4: validate() — invalid payloads fail for representative types
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
describe("validate – invalid payloads", () => {
|
||||
test("agent: missing required field fails", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { agent } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(agent, { package: "gpt-4o", version: "1" });
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(false);
|
||||
});
|
||||
|
||||
test("agent: wrong type for config fails", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { agent } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(agent, {
|
||||
package: "gpt-4o",
|
||||
version: "1",
|
||||
config: "not-an-object",
|
||||
});
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(false);
|
||||
});
|
||||
|
||||
test("role: missing systemPrompt fails", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { role } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(role, {
|
||||
name: "analyst",
|
||||
description: "d",
|
||||
extractPrompt: "e",
|
||||
schema: "AAAAAAAAAAAAA",
|
||||
});
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(false);
|
||||
});
|
||||
|
||||
test("thread-start: missing depth fails", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { threadStart } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(threadStart, {
|
||||
workflow: "AAAAAAAAAAAAA",
|
||||
input: "hi",
|
||||
parentThread: null,
|
||||
agents: {},
|
||||
});
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(false);
|
||||
});
|
||||
|
||||
test("thread-end: returnCode as string fails", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { threadEnd } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(threadEnd, {
|
||||
returnCode: "ok",
|
||||
summary: "Done",
|
||||
start: "AAAAAAAAAAAAA",
|
||||
lastStep: "AAAAAAAAAAAAA",
|
||||
});
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(false);
|
||||
});
|
||||
|
||||
test("content: missing text fails", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { content } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(content, {});
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(false);
|
||||
});
|
||||
|
||||
test("react-turn: tokens.input as string fails", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { reactTurn } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(reactTurn, {
|
||||
input: "AAAAAAAAAAAAA",
|
||||
output: "AAAAAAAAAAAAA",
|
||||
toolCalls: [],
|
||||
tokens: { input: "many", output: 50 },
|
||||
latencyMs: 100,
|
||||
});
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(false);
|
||||
});
|
||||
|
||||
test("react-tool-call: missing durationMs fails", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { reactToolCall } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(reactToolCall, {
|
||||
name: "tool",
|
||||
arguments: "AAAAAAAAAAAAA",
|
||||
result: "AAAAAAAAAAAAA",
|
||||
});
|
||||
expect(validate(store, store.get(h) as CasNode)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Step 5: refs() — extracts direct cas_ref fields from node payloads
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
describe("refs – cas_ref extraction", () => {
|
||||
const HASH_A = "AAAAAAAAAAAAA";
|
||||
const HASH_B = "BBBBBBBBBBBBB";
|
||||
|
||||
test("content node has no cas_ref fields → empty array", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { content } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(content, { text: "hello" });
|
||||
const node = store.get(h) as CasNode;
|
||||
expect(refs(store, node)).toEqual([]);
|
||||
});
|
||||
|
||||
test("role node: refs() returns the schema cas_ref", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { role } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(role, {
|
||||
name: "r",
|
||||
description: "d",
|
||||
systemPrompt: "s",
|
||||
extractPrompt: "e",
|
||||
schema: HASH_A,
|
||||
});
|
||||
const node = store.get(h) as CasNode;
|
||||
expect(refs(store, node)).toContain(HASH_A);
|
||||
});
|
||||
|
||||
test("thread-end: refs() returns start and lastStep", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { threadEnd } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(threadEnd, {
|
||||
returnCode: 0,
|
||||
summary: "done",
|
||||
start: HASH_A,
|
||||
lastStep: HASH_B,
|
||||
});
|
||||
const node = store.get(h) as CasNode;
|
||||
const result = refs(store, node);
|
||||
expect(result).toContain(HASH_A);
|
||||
expect(result).toContain(HASH_B);
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("react-tool-call: refs() returns arguments and result", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { reactToolCall } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(reactToolCall, {
|
||||
name: "search",
|
||||
arguments: HASH_A,
|
||||
result: HASH_B,
|
||||
durationMs: 100,
|
||||
});
|
||||
const node = store.get(h) as CasNode;
|
||||
const result = refs(store, node);
|
||||
expect(result).toContain(HASH_A);
|
||||
expect(result).toContain(HASH_B);
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("thread-step: refs() returns content, react, and start (previous null is skipped)", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { threadStep } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(threadStep, {
|
||||
role: "r",
|
||||
meta: {},
|
||||
content: HASH_A,
|
||||
react: HASH_B,
|
||||
start: HASH_A,
|
||||
previous: null,
|
||||
});
|
||||
const node = store.get(h) as CasNode;
|
||||
const result = refs(store, node);
|
||||
expect(result).toContain(HASH_A);
|
||||
expect(result).toContain(HASH_B);
|
||||
});
|
||||
|
||||
test("thread-step: refs() includes previous when non-null", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { threadStep } = await registerWorkflowSchemas(store);
|
||||
const HASH_C = "CCCCCCCCCCCCC";
|
||||
const h = await store.put(threadStep, {
|
||||
role: "r",
|
||||
meta: {},
|
||||
content: HASH_A,
|
||||
react: HASH_B,
|
||||
start: HASH_A,
|
||||
previous: HASH_C,
|
||||
});
|
||||
const node = store.get(h) as CasNode;
|
||||
const result = refs(store, node);
|
||||
expect(result).toContain(HASH_C);
|
||||
});
|
||||
|
||||
test("react-session: refs() returns the agent cas_ref", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { reactSession } = await registerWorkflowSchemas(store);
|
||||
const h = await store.put(reactSession, {
|
||||
agent: HASH_A,
|
||||
role: "r",
|
||||
turns: [],
|
||||
totalTokens: 0,
|
||||
durationMs: 0,
|
||||
});
|
||||
const node = store.get(h) as CasNode;
|
||||
expect(refs(store, node)).toContain(HASH_A);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Step 6: walk() — BFS traversal through linked workflow nodes
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
describe("walk – cross-schema traversal", () => {
|
||||
test("walk visits content node linked from thread-end", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { threadEnd, content } = await registerWorkflowSchemas(store);
|
||||
|
||||
const contentHash = await store.put(content, { text: "summary text" });
|
||||
const endHash = await store.put(threadEnd, {
|
||||
returnCode: 0,
|
||||
summary: "done",
|
||||
start: contentHash,
|
||||
lastStep: contentHash,
|
||||
});
|
||||
|
||||
const visited = new Set<string>();
|
||||
walk(store, endHash, (h) => visited.add(h));
|
||||
|
||||
expect(visited.has(endHash)).toBe(true);
|
||||
expect(visited.has(contentHash)).toBe(true);
|
||||
});
|
||||
|
||||
test("walk through role → (schema stored in store)", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { role, roleSchema } = await registerWorkflowSchemas(store);
|
||||
|
||||
const schemaDocHash = await store.put(roleSchema, {
|
||||
type: "object",
|
||||
properties: { answer: { type: "string" } },
|
||||
});
|
||||
const roleHash = await store.put(role, {
|
||||
name: "analyst",
|
||||
description: "d",
|
||||
systemPrompt: "s",
|
||||
extractPrompt: "e",
|
||||
schema: schemaDocHash,
|
||||
});
|
||||
|
||||
const visited = new Set<string>();
|
||||
walk(store, roleHash, (h) => visited.add(h));
|
||||
|
||||
expect(visited.has(roleHash)).toBe(true);
|
||||
expect(visited.has(schemaDocHash)).toBe(true);
|
||||
});
|
||||
|
||||
test("walk handles diamond: two thread-end nodes sharing the same start", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { threadEnd, content } = await registerWorkflowSchemas(store);
|
||||
|
||||
const sharedStart = await store.put(content, { text: "start" });
|
||||
const step1 = await store.put(content, { text: "step1" });
|
||||
const step2 = await store.put(content, { text: "step2" });
|
||||
|
||||
const end1 = await store.put(threadEnd, {
|
||||
returnCode: 0,
|
||||
summary: "path A",
|
||||
start: sharedStart,
|
||||
lastStep: step1,
|
||||
});
|
||||
const end2 = await store.put(threadEnd, {
|
||||
returnCode: 1,
|
||||
summary: "path B",
|
||||
start: sharedStart,
|
||||
lastStep: step2,
|
||||
});
|
||||
|
||||
// Use react-turn as the root linking both ends via input/output
|
||||
const { reactTurn } = await registerWorkflowSchemas(store);
|
||||
const turnHash = await store.put(reactTurn, {
|
||||
input: end1,
|
||||
output: end2,
|
||||
toolCalls: [],
|
||||
tokens: { input: 10, output: 5 },
|
||||
latencyMs: 50,
|
||||
});
|
||||
|
||||
const visited = new Set<string>();
|
||||
walk(store, turnHash, (h) => visited.add(h));
|
||||
|
||||
expect(visited.has(turnHash)).toBe(true);
|
||||
expect(visited.has(end1)).toBe(true);
|
||||
expect(visited.has(end2)).toBe(true);
|
||||
// sharedStart is reached from both end1 and end2, but visited only once
|
||||
expect(visited.has(sharedStart)).toBe(true);
|
||||
expect(visited.has(step1)).toBe(true);
|
||||
expect(visited.has(step2)).toBe(true);
|
||||
});
|
||||
|
||||
test("walk visits react-tool-call linked from react-turn", async () => {
|
||||
const store = createMemoryStore();
|
||||
const { reactTurn, reactToolCall, content } =
|
||||
await registerWorkflowSchemas(store);
|
||||
|
||||
const argsHash = await store.put(content, { text: '{"q":"test"}' });
|
||||
const resultHash = await store.put(content, { text: '{"r":"ok"}' });
|
||||
const toolCallHash = await store.put(reactToolCall, {
|
||||
name: "search",
|
||||
arguments: argsHash,
|
||||
result: resultHash,
|
||||
durationMs: 120,
|
||||
});
|
||||
|
||||
const inputHash = await store.put(content, { text: "input" });
|
||||
const outputHash = await store.put(content, { text: "output" });
|
||||
const turnHash = await store.put(reactTurn, {
|
||||
input: inputHash,
|
||||
output: outputHash,
|
||||
toolCalls: [],
|
||||
tokens: { input: 80, output: 40 },
|
||||
latencyMs: 600,
|
||||
});
|
||||
|
||||
const visited = new Set<string>();
|
||||
walk(store, turnHash, (h) => visited.add(h));
|
||||
|
||||
expect(visited.has(turnHash)).toBe(true);
|
||||
expect(visited.has(inputHash)).toBe(true);
|
||||
expect(visited.has(outputHash)).toBe(true);
|
||||
// toolCallHash is not in the turn's cas_ref fields (toolCalls array), only linked manually
|
||||
expect(visited.has(toolCallHash)).toBe(false);
|
||||
|
||||
// walk from toolCallHash to verify it reaches args and result
|
||||
const tcVisited = new Set<string>();
|
||||
walk(store, toolCallHash, (h) => tcVisited.add(h));
|
||||
expect(tcVisited.has(toolCallHash)).toBe(true);
|
||||
expect(tcVisited.has(argsHash)).toBe(true);
|
||||
expect(tcVisited.has(resultHash)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
export {
|
||||
registerWorkflowSchemas,
|
||||
type WorkflowSchemaHashes,
|
||||
} from "./schemas.js";
|
||||
export type {
|
||||
AgentPayload,
|
||||
ContentPayload,
|
||||
ReactSessionPayload,
|
||||
ReactToolCallPayload,
|
||||
ReactTurnPayload,
|
||||
ReactTurnTokens,
|
||||
RolePayload,
|
||||
RoleSchemaPayload,
|
||||
ThreadEndPayload,
|
||||
ThreadStartPayload,
|
||||
ThreadStepPayload,
|
||||
WorkflowPayload,
|
||||
WorkflowTransition,
|
||||
} from "./types.js";
|
||||
@@ -1,236 +0,0 @@
|
||||
import type { Hash, Store } from "@uncaged/json-cas";
|
||||
import { type JSONSchema, putSchema } from "@uncaged/json-cas";
|
||||
|
||||
// ── Definition layer ──────────────────────────────────────────────────────────
|
||||
|
||||
const AGENT: JSONSchema = {
|
||||
type: "object",
|
||||
required: ["package", "version", "config"],
|
||||
properties: {
|
||||
package: { type: "string" },
|
||||
version: { type: "string" },
|
||||
config: { type: "object" },
|
||||
},
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
/** role-schema nodes hold raw JSON Schema documents, so any object is valid. */
|
||||
const ROLE_SCHEMA: JSONSchema = {
|
||||
type: "object",
|
||||
};
|
||||
|
||||
const ROLE: JSONSchema = {
|
||||
type: "object",
|
||||
required: ["name", "description", "systemPrompt", "extractPrompt", "schema"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
description: { type: "string" },
|
||||
systemPrompt: { type: "string" },
|
||||
extractPrompt: { type: "string" },
|
||||
schema: { type: "string", format: "cas_ref" },
|
||||
},
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
const WORKFLOW: JSONSchema = {
|
||||
type: "object",
|
||||
required: ["name", "description", "roles", "moderator"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
description: { type: "string" },
|
||||
roles: {
|
||||
type: "object",
|
||||
additionalProperties: { type: "string", format: "cas_ref" },
|
||||
},
|
||||
moderator: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
required: ["from", "to", "when"],
|
||||
properties: {
|
||||
from: { type: "string" },
|
||||
to: { type: "string" },
|
||||
when: { anyOf: [{ type: "string" }, { type: "null" }] },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
// ── Execution layer ───────────────────────────────────────────────────────────
|
||||
|
||||
const THREAD_START: JSONSchema = {
|
||||
type: "object",
|
||||
required: ["workflow", "input", "depth", "parentThread", "agents"],
|
||||
properties: {
|
||||
workflow: { type: "string", format: "cas_ref" },
|
||||
input: { type: "string" },
|
||||
depth: { type: "number" },
|
||||
parentThread: {
|
||||
anyOf: [{ type: "string", format: "cas_ref" }, { type: "null" }],
|
||||
},
|
||||
agents: {
|
||||
type: "object",
|
||||
additionalProperties: { type: "string", format: "cas_ref" },
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
const THREAD_STEP: JSONSchema = {
|
||||
type: "object",
|
||||
required: ["role", "meta", "content", "react", "start", "previous"],
|
||||
properties: {
|
||||
role: { type: "string" },
|
||||
meta: { type: "object" },
|
||||
content: { type: "string", format: "cas_ref" },
|
||||
react: { type: "string", format: "cas_ref" },
|
||||
start: { type: "string", format: "cas_ref" },
|
||||
previous: {
|
||||
anyOf: [{ type: "string", format: "cas_ref" }, { type: "null" }],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
const THREAD_END: JSONSchema = {
|
||||
type: "object",
|
||||
required: ["returnCode", "summary", "start", "lastStep"],
|
||||
properties: {
|
||||
returnCode: { type: "number" },
|
||||
summary: { type: "string" },
|
||||
start: { type: "string", format: "cas_ref" },
|
||||
lastStep: { type: "string", format: "cas_ref" },
|
||||
},
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
const CONTENT: JSONSchema = {
|
||||
type: "object",
|
||||
required: ["text"],
|
||||
properties: {
|
||||
text: { type: "string" },
|
||||
},
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
// ── React layer ───────────────────────────────────────────────────────────────
|
||||
|
||||
const REACT_SESSION: JSONSchema = {
|
||||
type: "object",
|
||||
required: ["agent", "role", "turns", "totalTokens", "durationMs"],
|
||||
properties: {
|
||||
agent: { type: "string", format: "cas_ref" },
|
||||
role: { type: "string" },
|
||||
turns: {
|
||||
type: "array",
|
||||
items: { type: "string", format: "cas_ref" },
|
||||
},
|
||||
totalTokens: { type: "number" },
|
||||
durationMs: { type: "number" },
|
||||
},
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
const REACT_TURN: JSONSchema = {
|
||||
type: "object",
|
||||
required: ["input", "output", "toolCalls", "tokens", "latencyMs"],
|
||||
properties: {
|
||||
input: { type: "string", format: "cas_ref" },
|
||||
output: { type: "string", format: "cas_ref" },
|
||||
toolCalls: {
|
||||
type: "array",
|
||||
items: { type: "string", format: "cas_ref" },
|
||||
},
|
||||
tokens: {
|
||||
type: "object",
|
||||
required: ["input", "output"],
|
||||
properties: {
|
||||
input: { type: "number" },
|
||||
output: { type: "number" },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
latencyMs: { type: "number" },
|
||||
},
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
const REACT_TOOL_CALL: JSONSchema = {
|
||||
type: "object",
|
||||
required: ["name", "arguments", "result", "durationMs"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
arguments: { type: "string", format: "cas_ref" },
|
||||
result: { type: "string", format: "cas_ref" },
|
||||
durationMs: { type: "number" },
|
||||
},
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
// ── Registry ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export type WorkflowSchemaHashes = {
|
||||
agent: Hash;
|
||||
roleSchema: Hash;
|
||||
role: Hash;
|
||||
workflow: Hash;
|
||||
threadStart: Hash;
|
||||
threadStep: Hash;
|
||||
threadEnd: Hash;
|
||||
content: Hash;
|
||||
reactSession: Hash;
|
||||
reactTurn: Hash;
|
||||
reactToolCall: Hash;
|
||||
};
|
||||
|
||||
/**
|
||||
* Register all 11 workflow schemas into the given store.
|
||||
* Returns a map from camelCase schema name to its CAS type hash.
|
||||
* Idempotent: safe to call multiple times on the same store.
|
||||
*/
|
||||
export async function registerWorkflowSchemas(
|
||||
store: Store,
|
||||
): Promise<WorkflowSchemaHashes> {
|
||||
const [
|
||||
agent,
|
||||
roleSchema,
|
||||
role,
|
||||
workflow,
|
||||
threadStart,
|
||||
threadStep,
|
||||
threadEnd,
|
||||
content,
|
||||
reactSession,
|
||||
reactTurn,
|
||||
reactToolCall,
|
||||
] = await Promise.all([
|
||||
putSchema(store, AGENT),
|
||||
putSchema(store, ROLE_SCHEMA),
|
||||
putSchema(store, ROLE),
|
||||
putSchema(store, WORKFLOW),
|
||||
putSchema(store, THREAD_START),
|
||||
putSchema(store, THREAD_STEP),
|
||||
putSchema(store, THREAD_END),
|
||||
putSchema(store, CONTENT),
|
||||
putSchema(store, REACT_SESSION),
|
||||
putSchema(store, REACT_TURN),
|
||||
putSchema(store, REACT_TOOL_CALL),
|
||||
]);
|
||||
|
||||
return {
|
||||
agent,
|
||||
roleSchema,
|
||||
role,
|
||||
workflow,
|
||||
threadStart,
|
||||
threadStep,
|
||||
threadEnd,
|
||||
content,
|
||||
reactSession,
|
||||
reactTurn,
|
||||
reactToolCall,
|
||||
};
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import type { Hash } from "@uncaged/json-cas";
|
||||
|
||||
// ── Definition layer ──────────────────────────────────────────────────────────
|
||||
|
||||
export type AgentPayload = {
|
||||
package: string;
|
||||
version: string;
|
||||
config: Record<string, unknown>;
|
||||
};
|
||||
|
||||
/** A JSON Schema document stored as-is. */
|
||||
export type RoleSchemaPayload = Record<string, unknown>;
|
||||
|
||||
export type RolePayload = {
|
||||
name: string;
|
||||
description: string;
|
||||
systemPrompt: string;
|
||||
extractPrompt: string;
|
||||
/** cas_ref → role-schema */
|
||||
schema: Hash;
|
||||
};
|
||||
|
||||
export type WorkflowTransition = {
|
||||
from: string;
|
||||
to: string;
|
||||
when: string | null;
|
||||
};
|
||||
|
||||
export type WorkflowPayload = {
|
||||
name: string;
|
||||
description: string;
|
||||
/** cas_ref → role */
|
||||
roles: Record<string, Hash>;
|
||||
moderator: WorkflowTransition[];
|
||||
};
|
||||
|
||||
// ── Execution layer ───────────────────────────────────────────────────────────
|
||||
|
||||
export type ThreadStartPayload = {
|
||||
/** cas_ref → workflow */
|
||||
workflow: Hash;
|
||||
input: string;
|
||||
depth: number;
|
||||
/** cas_ref → thread-start | null */
|
||||
parentThread: Hash | null;
|
||||
/** cas_ref → agent */
|
||||
agents: Record<string, Hash>;
|
||||
};
|
||||
|
||||
export type ThreadStepPayload = {
|
||||
role: string;
|
||||
meta: Record<string, unknown>;
|
||||
/** cas_ref → content */
|
||||
content: Hash;
|
||||
/** cas_ref → react-session */
|
||||
react: Hash;
|
||||
/** cas_ref → thread-start */
|
||||
start: Hash;
|
||||
/** cas_ref → thread-step | null */
|
||||
previous: Hash | null;
|
||||
};
|
||||
|
||||
export type ThreadEndPayload = {
|
||||
returnCode: number;
|
||||
summary: string;
|
||||
/** cas_ref → thread-start */
|
||||
start: Hash;
|
||||
/** cas_ref → thread-step */
|
||||
lastStep: Hash;
|
||||
};
|
||||
|
||||
export type ContentPayload = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
// ── React layer ───────────────────────────────────────────────────────────────
|
||||
|
||||
export type ReactSessionPayload = {
|
||||
/** cas_ref → agent */
|
||||
agent: Hash;
|
||||
role: string;
|
||||
/** cas_ref → react-turn */
|
||||
turns: Hash[];
|
||||
totalTokens: number;
|
||||
durationMs: number;
|
||||
};
|
||||
|
||||
export type ReactTurnTokens = {
|
||||
input: number;
|
||||
output: number;
|
||||
};
|
||||
|
||||
export type ReactTurnPayload = {
|
||||
/** cas_ref → content */
|
||||
input: Hash;
|
||||
/** cas_ref → content */
|
||||
output: Hash;
|
||||
/** cas_ref → react-tool-call */
|
||||
toolCalls: Hash[];
|
||||
tokens: ReactTurnTokens;
|
||||
latencyMs: number;
|
||||
};
|
||||
|
||||
export type ReactToolCallPayload = {
|
||||
name: string;
|
||||
/** cas_ref → content (arguments) */
|
||||
arguments: Hash;
|
||||
/** cas_ref → content (result) */
|
||||
result: Hash;
|
||||
durationMs: number;
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts"],
|
||||
"references": [{ "path": "../json-cas" }]
|
||||
}
|
||||
+1
-4
@@ -12,10 +12,7 @@
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@uncaged/json-cas": ["./packages/json-cas/src/index.ts"],
|
||||
"@uncaged/json-cas-fs": ["./packages/json-cas-fs/src/index.ts"],
|
||||
"@uncaged/json-cas-workflow": [
|
||||
"./packages/json-cas-workflow/src/index.ts"
|
||||
]
|
||||
"@uncaged/json-cas-fs": ["./packages/json-cas-fs/src/index.ts"]
|
||||
},
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
|
||||
Reference in New Issue
Block a user