Merge pull request 'fix: PR #81 review follow-ups (closes #83)' (#84) from fix/pr81-review-followups into main

This commit was merged in pull request #84.
This commit is contained in:
2026-04-24 11:10:45 +00:00
6 changed files with 31 additions and 30 deletions
+10 -15
View File
@@ -193,15 +193,14 @@ export type PartitionedMessage = {
* Extract display fields from a WorkflowMessage-shaped object.
* `role` and `content` are used for header/body; `meta` is serialized as YAML frontmatter.
*/
export function partitionWorkflowMessage(msg: Record<string, unknown>): PartitionedMessage {
const roleStr = typeof msg.role === "string" ? msg.role : "?";
const contentRaw = msg.content;
const contentBody =
contentRaw === undefined || contentRaw === null
? ""
: typeof contentRaw === "string"
? contentRaw
: JSON.stringify(contentRaw);
export function partitionWorkflowMessage(msg: {
role: string;
content: string;
meta: unknown;
timestamp: number;
}): PartitionedMessage {
const roleStr = msg.role;
const contentBody = msg.content;
const meta: Record<string, unknown> =
msg.meta !== null && msg.meta !== undefined && typeof msg.meta === "object"
? (msg.meta as Record<string, unknown>)
@@ -213,9 +212,7 @@ export function partitionWorkflowMessage(msg: Record<string, unknown>): Partitio
* One role round as plain text: header line, YAML frontmatter (meta only), body (content).
*/
export function formatThreadRoundBlock(row: ThreadRoundRow): string {
const { roleStr, contentBody, meta } = partitionWorkflowMessage(
row.message as unknown as Record<string, unknown>,
);
const { roleStr, contentBody, meta } = partitionWorkflowMessage(row.message);
const yamlBlock =
Object.keys(meta).length === 0 ? "{}\n" : `${stringify(meta, { lineWidth: 100 })}\n`;
return (
@@ -257,9 +254,7 @@ export function buildThreadCommandOutput(
continue;
}
if (picked.length === 0) {
const { roleStr, contentBody, meta } = partitionWorkflowMessage(
row.message as unknown as Record<string, unknown>,
);
const { roleStr, contentBody, meta } = partitionWorkflowMessage(row.message);
const yamlBlock =
Object.keys(meta).length === 0 ? "{}\n" : `${stringify(meta, { lineWidth: 100 })}\n`;
const header =
+1 -2
View File
@@ -3,8 +3,7 @@ import { parse } from "yaml";
import type { Result } from "./result.js";
import { err, ok } from "./result.js";
import type { NerveConfig, ReflexConfig, SenseConfig, WorkflowConfig } from "./types.js";
const DEFAULT_ENGINE_MAX_ROUNDS = 100;
import { DEFAULT_ENGINE_MAX_ROUNDS } from "./types.js";
const DURATION_RE = /^(\d+)([smh])$/;
+1 -8
View File
@@ -18,17 +18,10 @@ export type {
WorkflowDefinition,
SenseResult,
} from "./types.js";
export { START, END } from "./types.js";
export { START, END, DEFAULT_ENGINE_MAX_ROUNDS } from "./types.js";
export type { Result } from "./result.js";
export { ok, err } from "./result.js";
export { parseNerveConfig } from "./config.js";
export function parseWorkflowField(field: string): { name: string; maxRounds: number; prompt: string } {
const [name, rounds, ...rest] = field.split("|");
const prompt = rest.join("|");
const maxRounds = parseInt(rounds, 10);
return { name: name ?? "", maxRounds, prompt };
}
export type { ParsedSenseWorkflowDirective, SenseComputeRoute } from "./sense-workflow-directive.js";
export { parseSenseWorkflowDirective, routeSenseComputeOutput } from "./sense-workflow-directive.js";
+3
View File
@@ -61,6 +61,9 @@ export const END = "__end__" as const;
export type START = typeof START;
export type END = typeof END;
/** Engine-wide fallback for max moderator rounds when not specified in config. */
export const DEFAULT_ENGINE_MAX_ROUNDS = 100;
/** A single message in the workflow conversation chain (runtime, type-erased). */
export type WorkflowMessage = {
role: string;
+7 -5
View File
@@ -147,14 +147,14 @@ export type LogStore = {
runId: string,
) => Array<{ role: string; content: string; meta: unknown; timestamp: number }>;
/**
* Count role command events for a run (excludes `thread_start` and invalid payloads).
* Count role command events for a run (excludes `thread_start`/`__start__` messages and invalid payloads).
* Round indices for {@link getThreadRounds} are 1..count in chronological order.
*/
getThreadRoundCount: (runId: string) => number;
/**
* Role rounds for agent-oriented retrieval: each row is one `thread_command_event`
* whose JSON `type` is not `thread_start`, with `round` from ROW_NUMBER() OVER (ORDER BY id ASC).
* No schema migration — numbering is computed in SQL.
* Role rounds for agent-oriented retrieval: each row is one `thread_command_event` or
* `thread_workflow_message` whose JSON `type` is not `thread_start` and `role` is not `__start__`,
* with `round` from ROW_NUMBER() OVER (ORDER BY id ASC). No schema migration — numbering is computed in SQL.
*/
getThreadRounds: (runId: string, params: GetThreadRoundsParams) => ThreadRoundRow[];
/**
@@ -324,7 +324,8 @@ export function createLogStore(dbPath: string): LogStore {
`SELECT COUNT(*) AS c FROM logs
WHERE source = 'workflow' AND type IN ('thread_command_event', 'thread_workflow_message') AND ref_id = ?
AND payload IS NOT NULL AND json_valid(payload) = 1
AND COALESCE(json_extract(payload, '$.type'), '') != 'thread_start'`,
AND COALESCE(json_extract(payload, '$.type'), '') != 'thread_start'
AND COALESCE(json_extract(payload, '$.role'), '') != '__start__'`,
);
const getThreadRoundsStmt = sqlite.prepare(
@@ -335,6 +336,7 @@ export function createLogStore(dbPath: string): LogStore {
WHERE source = 'workflow' AND type IN ('thread_command_event', 'thread_workflow_message') AND ref_id = @runId
AND payload IS NOT NULL AND json_valid(payload) = 1
AND COALESCE(json_extract(payload, '$.type'), '') != 'thread_start'
AND COALESCE(json_extract(payload, '$.role'), '') != '__start__'
)
SELECT id, ts, payload, rn FROM numbered
WHERE (@before = 0 OR rn < @before)
+9
View File
@@ -130,6 +130,15 @@ async function runThread(
return;
}
if (typeof result.content !== "string") {
sendWorkflowError(runId, `Role "${nextRole}" returned non-string content`);
return;
}
if (result.meta === null || typeof result.meta !== "object" || Array.isArray(result.meta)) {
sendWorkflowError(runId, `Role "${nextRole}" returned invalid meta (must be a plain object)`);
return;
}
const message: WorkflowMessage = {
role: nextRole,
content: result.content,