Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/upgrade-workflow-beta-44.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Upgrade the bundled Workflow DevKit packages to the latest 5.0 beta releases, including sealed-log event ordering and long-polling run completion.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const TURN_COUNT = 100;
export default defineEval({
description: "Workflow stress: one durable session completes 100 sequential turns.",
tags: ["stress", "workflow", "sequential"],
timeoutMs: 900_000,

async test(t) {
let sessionId: string | undefined;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { defineTool } from "eve/tools";
import { once } from "eve/tools/approval";
import { z } from "zod";

export default defineTool({
description: "Keep an admitted continuation nonterminal long enough for a later-turn check.",
description: "Keep an admitted continuation nonterminal until its parent releases it.",
inputSchema: z.object({ marker: z.literal("HOLD") }),
execute: async ({ marker }) => {
await new Promise((resolve) => setTimeout(resolve, 5_000));
return { marker, released: true };
},
approval: once(),
execute: async ({ marker }) => ({ marker, released: true }),
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
parseToolErrorOutput,
sendAndFollowQueuedTurn,
waitForCompletedTask,
waitForTaskInput,
} from "./shared.js";

/** A persistent child with a nonterminal task rejects every competing continuation. */
Expand All @@ -14,6 +15,8 @@ export default defineTaskEval({
primary: "task.agent.continue.rejected-agent-busy",
setup: [
"task.dispatch.start.accepted-acknowledged",
"task.input.require.accepted-valid-batch",
"task.input.answer.accepted-complete",
"task.lifecycle.complete.accepted-nonterminal",
"task.agent.continue.accepted-terminal-available",
],
Expand Down Expand Up @@ -69,7 +72,17 @@ export default defineTaskEval({
status: "failed",
});

await waitForCompletedTask(t, later.session, "CHILD-TASK-EXCLUSIVITY-VERIFY", admittedTaskId);
const blocked = await waitForTaskInput(t, later.session, "hold");
const released = await blocked.session.respond([
{
optionId: "approve",
requestId: blocked.request.requestId,
},
]);
released.expectOk();
released.noFailedActions();

await waitForCompletedTask(t, blocked.session, "CHILD-TASK-EXCLUSIVITY-VERIFY", admittedTaskId);
},
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import type { EveEvalTurn } from "eve/evals";
import type { EveEvalContext, EveEvalTurn } from "eve/evals";
import { satisfies } from "eve/evals/expect";

import { defineTaskEval } from "./task-transition.js";
import { requireSessionStreamIndex } from "./shared.js";
import {
requireSessionStreamIndex,
type TaskEvalSessionDriver,
waitForTaskInput,
} from "./shared.js";

const REVIEW_FINDING = "blocker: task admission can discard deferred user input.";

Expand All @@ -14,6 +18,8 @@ export default defineTaskEval({
primary: "task.parent.wake.emitted-ready",
setup: [
"task.dispatch.start.accepted-acknowledged",
"task.input.require.accepted-valid-batch",
"task.input.answer.accepted-complete",
"task.lifecycle.complete.accepted-nonterminal",
],
dimensions: { transport: "local", parentPhase: "parked" },
Expand All @@ -36,11 +42,14 @@ export default defineTaskEval({
),
);

const sessionId = started.sessionId;
const firstLive = t.target.watchTurn(sessionId, {
startIndex: requireSessionStreamIndex(t, "First reviewer wake"),
});
const firstWake = await firstLive.result();
const blocked = await waitForTaskInput(t, t, "hold");
const firstObserved = await waitForTaskNotification(
t,
blocked.session,
blocked.observedTurns,
"First reviewer wake",
);
const firstWake = firstObserved.turn;

firstWake.expectOk();
const firstNotification = requireTaskNotification(firstWake);
Expand All @@ -65,10 +74,22 @@ export default defineTaskEval({
satisfies((message) => message === undefined, "the pending cohort wake is silent"),
);

const secondLive = t.target.watchTurn(sessionId, {
startIndex: requireSessionStreamIndex(firstLive.session, "Late reviewer wake"),
});
const wake = await secondLive.result();
const released = await firstObserved.session.respond([
{
optionId: "approve",
requestId: blocked.request.requestId,
},
]);
released.expectOk();
released.noFailedActions();

const lateObserved = await waitForTaskNotification(
t,
firstObserved.session,
[released],
"Late reviewer wake",
);
const wake = lateObserved.turn;

wake.expectOk();
const lateNotification = requireTaskNotification(wake);
Expand Down Expand Up @@ -98,11 +119,43 @@ interface TaskNotification {
readonly taskId: string;
}

interface ObservedTaskNotification {
readonly session: TaskEvalSessionDriver;
readonly turn: EveEvalTurn;
}

async function waitForTaskNotification(
t: EveEvalContext,
initialSession: TaskEvalSessionDriver,
observedTurns: readonly EveEvalTurn[],
operation: string,
): Promise<ObservedTaskNotification> {
const observed = observedTurns.find((turn) => taskNotification(turn) !== undefined);
if (observed !== undefined) return { session: initialSession, turn: observed };

const sessionId = initialSession.sessionId;
if (sessionId === undefined) throw new Error(`${operation} has no parent session id.`);
const live = t.target.watchTurn(sessionId, {
startIndex: requireSessionStreamIndex(initialSession, operation),
});
const turn = await live.result();
if (taskNotification(turn) === undefined) {
throw new Error(`${operation} has no completed-task notification.`);
}
return { session: live.session, turn };
}

function requireTaskNotification(turn: EveEvalTurn): TaskNotification {
const notification = taskNotification(turn);
if (notification !== undefined) return notification;
throw new Error("Reviewer wake has no completed-task notification.");
}

function taskNotification(turn: EveEvalTurn): TaskNotification | undefined {
for (const event of turn.events) {
if (event.type !== "message.received" || typeof event.data.message !== "string") continue;
const taskId = /Background task (task_[a-z0-9]+)/iu.exec(event.data.message)?.[1];
if (taskId !== undefined) return { message: event.data.message, taskId };
}
throw new Error("Reviewer wake has no completed-task notification.");
return undefined;
}
12 changes: 6 additions & 6 deletions packages/eve/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -420,13 +420,13 @@
"@vercel/sandbox": "catalog:",
"@vercel/sandbox-drives": "catalog:",
"@vercel/sdk": "1.28.8",
"@workflow/core": "5.0.0-beta.43",
"@workflow/errors": "5.0.0-beta.17",
"@workflow/core": "5.0.0-beta.44",
"@workflow/errors": "5.0.0-beta.18",
"@workflow/serde": "5.0.0-beta.2",
"@workflow/utils": "5.0.0-beta.8",
"@workflow/world": "5.0.0-beta.28",
"@workflow/world-local": "5.0.0-beta.37",
"@workflow/world-vercel": "5.0.0-beta.39",
"@workflow/utils": "5.0.0-beta.9",
"@workflow/world": "5.0.0-beta.29",
"@workflow/world-local": "5.0.0-beta.38",
"@workflow/world-vercel": "5.0.0-beta.40",
"ai": "catalog:",
"autoevals": "0.0.132",
"chat": "4.34.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ async function call<T>(
export function createDevelopmentWorkflowWorld(): World {
const forwarded = buildForwardedOperations();
const world = {
specVersion: 6 as SpecVersion,
specVersion: 7 as SpecVersion,
async getDeploymentId() {
// Inside a pinned delivery, steps and child runs must record the
// delivery's generation — not whatever is active — so replay after a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ describe("compiled vendor assets", () => {
readFile(join(COMPILED_VENDOR_ROOT, "@workflow/core/runtime/run.d.ts"), "utf8"),
]);

expect(indexDts).toContain("Just the core utilities");
expect(indexDts).toContain("Core utilities intended for import by user");
expect(indexDts).toContain("from '#compiled/@workflow/errors/index.js'");
expect(createHookDts).toContain("Creates a {@link Hook}");
expect(workflowDts).toBe(`export * from "./workflow/index.js";\n`);
Expand Down
Loading
Loading