refactor(cli,docs): RFC-005 Phase 4 — update templates, tests, docs (closes #271)
This commit is contained in:
@@ -33,9 +33,11 @@ describe("buildWorkflowScaffold", () => {
|
||||
expect(indexTs).toContain("@uncaged/nerve-core");
|
||||
});
|
||||
|
||||
it("root index wires moderator and END", () => {
|
||||
it("root index wires moderator with ThreadContext and END", () => {
|
||||
const { indexTs } = buildWorkflowScaffold("test");
|
||||
expect(indexTs).toContain("moderator");
|
||||
expect(indexTs).toContain("ThreadContext");
|
||||
expect(indexTs).toContain("ctx.steps.length");
|
||||
expect(indexTs).toContain("END");
|
||||
});
|
||||
|
||||
@@ -46,9 +48,13 @@ describe("buildWorkflowScaffold", () => {
|
||||
expect(indexTs).toContain("./roles/main/index.js");
|
||||
});
|
||||
|
||||
it("main role module exports mainRole function", () => {
|
||||
it("main role module exports mainRole with ThreadContext", () => {
|
||||
const { roleMainIndexTs } = buildWorkflowScaffold("test");
|
||||
expect(roleMainIndexTs).toContain("export async function mainRole");
|
||||
expect(roleMainIndexTs).toContain("ThreadContext");
|
||||
expect(roleMainIndexTs).toContain("RoleResult");
|
||||
expect(roleMainIndexTs).not.toContain("StartStep");
|
||||
expect(roleMainIndexTs).not.toContain("WorkflowMessage");
|
||||
});
|
||||
|
||||
it("uses different names per call", () => {
|
||||
|
||||
@@ -41,6 +41,7 @@ import { existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync
|
||||
import { createRequire } from "node:module";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
import type { NerveConfig } from "@uncaged/nerve-core";
|
||||
import { createKernel } from "@uncaged/nerve-daemon";
|
||||
@@ -61,6 +62,9 @@ const nerveDaemonRoot = dirname(require.resolve("@uncaged/nerve-daemon/package.j
|
||||
const senseWorkerScript = join(nerveDaemonRoot, "dist", "sense-worker.js");
|
||||
const workflowWorkerScript = join(nerveDaemonRoot, "dist", "workflow-worker.js");
|
||||
|
||||
const requireDaemon = createRequire(join(nerveDaemonRoot, "package.json"));
|
||||
const drizzleSqliteCoreHref = pathToFileURL(requireDaemon.resolve("drizzle-orm/sqlite-core")).href;
|
||||
|
||||
const nerveYamlTemplate = `senses:
|
||||
counter:
|
||||
group: e2e
|
||||
@@ -88,17 +92,17 @@ const echoWorkflowIndexJs = `const END = "__end__";
|
||||
export default {
|
||||
name: "echo",
|
||||
roles: {
|
||||
echo: async (start, _messages) => {
|
||||
echo: async (ctx) => {
|
||||
await new Promise((r) => setTimeout(r, 350));
|
||||
const p = typeof start.content === "string" ? start.content : "";
|
||||
const p = typeof ctx.start.content === "string" ? ctx.start.content : "";
|
||||
return {
|
||||
content: p.length > 0 ? "echo:" + p : "echo:empty",
|
||||
meta: {},
|
||||
};
|
||||
},
|
||||
},
|
||||
moderator({ steps }) {
|
||||
if (steps.length === 0) return "echo";
|
||||
moderator(ctx) {
|
||||
if (ctx.steps.length === 0) return "echo";
|
||||
return END;
|
||||
},
|
||||
};
|
||||
@@ -121,30 +125,47 @@ api:
|
||||
host: 127.0.0.1
|
||||
`;
|
||||
|
||||
/** Empty migration — counter sense uses only `_signals` (auto-created by daemon). */
|
||||
const counterMigration = `-- no-op migration for e2e counter sense
|
||||
SELECT 1;
|
||||
/** Schema for \`table\` export — rows mirror signal payloads inserted by the runtime. */
|
||||
const counterMigration = `CREATE TABLE IF NOT EXISTS sense_payload (
|
||||
count INTEGER,
|
||||
launched INTEGER,
|
||||
idle INTEGER
|
||||
);
|
||||
`;
|
||||
|
||||
/**
|
||||
* Minimal counter sense — each compute returns an incrementing count.
|
||||
* Does NOT touch the DB directly; signal persistence is handled by the daemon
|
||||
* (`runtime.persistSignal`) which writes to `_signals` automatically.
|
||||
*/
|
||||
const counterIndexJs = `let _count = 0;
|
||||
export async function compute(_db, _peers, _options) {
|
||||
function buildCounterIndexJs(): string {
|
||||
return `import { integer, sqliteTable } from "${drizzleSqliteCoreHref}";
|
||||
|
||||
export const table = sqliteTable("sense_payload", {
|
||||
count: integer("count"),
|
||||
launched: integer("launched"),
|
||||
idle: integer("idle"),
|
||||
});
|
||||
|
||||
let _count = 0;
|
||||
export async function compute() {
|
||||
_count += 1;
|
||||
return { signal: { count: _count }, workflow: null };
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/** First trigger launches local noop workflow; later triggers emit a plain signal. */
|
||||
const counterIndexJsWithNoopWorkflow = `let _launched = false;
|
||||
export async function compute(_db, _peers, _options) {
|
||||
function buildCounterIndexJsWithNoopWorkflow(): string {
|
||||
return `import { integer, sqliteTable } from "${drizzleSqliteCoreHref}";
|
||||
|
||||
export const table = sqliteTable("sense_payload", {
|
||||
count: integer("count"),
|
||||
launched: integer("launched"),
|
||||
idle: integer("idle"),
|
||||
});
|
||||
|
||||
let _launched = false;
|
||||
export async function compute() {
|
||||
if (!_launched) {
|
||||
_launched = true;
|
||||
return {
|
||||
signal: { launched: true },
|
||||
signal: { launched: 1 },
|
||||
workflow: {
|
||||
name: "noop",
|
||||
maxRounds: 3,
|
||||
@@ -153,18 +174,25 @@ export async function compute(_db, _peers, _options) {
|
||||
},
|
||||
};
|
||||
}
|
||||
return { signal: { idle: true }, workflow: null };
|
||||
return { signal: { idle: 1 }, workflow: null };
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/** Minimal workflow: moderator ends immediately (no role rounds). */
|
||||
const noopWorkflowIndexJs = `const END = "__end__";
|
||||
export default {
|
||||
name: "noop",
|
||||
roles: {
|
||||
bot: async () => ({ content: "ok", meta: {} }),
|
||||
bot: async (ctx) => {
|
||||
void ctx;
|
||||
return { content: "ok", meta: {} };
|
||||
},
|
||||
},
|
||||
moderator(ctx) {
|
||||
void ctx;
|
||||
return END;
|
||||
},
|
||||
moderator: () => END,
|
||||
};
|
||||
`;
|
||||
|
||||
@@ -222,7 +250,7 @@ function writeWorkspaceLayout(nerveRoot: string, withNoopWorkflow: boolean): voi
|
||||
);
|
||||
writeFileSync(
|
||||
join(nerveRoot, "senses", "counter", "index.js"),
|
||||
withNoopWorkflow ? counterIndexJsWithNoopWorkflow : counterIndexJs,
|
||||
withNoopWorkflow ? buildCounterIndexJsWithNoopWorkflow() : buildCounterIndexJs(),
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(
|
||||
|
||||
@@ -52,7 +52,7 @@ export function buildWorkflowScaffold(name: string): WorkflowScaffoldFiles {
|
||||
}
|
||||
|
||||
function buildWorkflowIndexTs(name: string): string {
|
||||
return `import type { WorkflowDefinition } from "@uncaged/nerve-core";
|
||||
return `import type { ThreadContext, WorkflowDefinition } from "@uncaged/nerve-core";
|
||||
import { END } from "@uncaged/nerve-core";
|
||||
|
||||
import { mainRole } from "./roles/main/index.js";
|
||||
@@ -64,8 +64,8 @@ const workflow: WorkflowDefinition<Record<"main", MainMeta>> = {
|
||||
roles: {
|
||||
main: mainRole,
|
||||
},
|
||||
moderator({ steps }) {
|
||||
if (steps.length === 0) {
|
||||
moderator(ctx: ThreadContext<Record<"main", MainMeta>>) {
|
||||
if (ctx.steps.length === 0) {
|
||||
return "main";
|
||||
}
|
||||
return END;
|
||||
@@ -77,18 +77,16 @@ export default workflow;
|
||||
}
|
||||
|
||||
function buildWorkflowMainRoleIndexTs(name: string): string {
|
||||
return `import type { RoleResult, StartStep, WorkflowMessage } from "@uncaged/nerve-core";
|
||||
return `import type { RoleResult, ThreadContext } from "@uncaged/nerve-core";
|
||||
|
||||
/**
|
||||
* Main role — implement LLM calls, scripts, HTTP, etc.
|
||||
* Optional: align behavior with \`prompt.md\` in this directory.
|
||||
*/
|
||||
export async function mainRole(
|
||||
start: StartStep,
|
||||
messages: WorkflowMessage[],
|
||||
ctx: ThreadContext,
|
||||
): Promise<RoleResult<Record<string, unknown>>> {
|
||||
void start;
|
||||
void messages;
|
||||
void ctx;
|
||||
// TODO: implement your role logic here
|
||||
return {
|
||||
content: "${name} started",
|
||||
|
||||
Reference in New Issue
Block a user