mirror of
https://github.com/vercel/eve.git
synced 2026-09-20 05:35:39 +08:00
fix(eve): deployment upgrades - refresh existing session prompts (#206)
Signed-off-by: Andrew Barba <barba@hey.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"eve": patch
|
||||
---
|
||||
|
||||
Existing production sessions now refresh their system prompt from the latest deployment before each model step. Long-lived channel conversations retain their history and state while adopting updated agent instructions.
|
||||
@@ -17,6 +17,8 @@ Every turn runs as a durable workflow, built on the open-source [Workflow SDK](h
|
||||
|
||||
The Workflow SDK is not inherently tied to Vercel. In local development and in a self-deployed `eve start` process, eve uses the SDK's local world by default; that world persists workflow runs on disk, normally under `.workflow-data`, and dispatches through the same Nitro-hosted workflow routes. On Vercel, the same workflow code runs against Vercel Workflow instead, which adds platform features such as latest production deployment routing and dashboard run metadata.
|
||||
|
||||
When a Vercel production deployment changes, the next model turn in an existing session uses that deployment's current instructions, model, and tools. The durable session keeps its conversation history and authored state, so identity-based channels such as Telegram private chats and Twilio phone-number conversations adopt agent updates without requiring a new session.
|
||||
|
||||
Nitro hosts the HTTP routes and workflow entrypoints. It does not supply the workflow state store or the sandbox runtime. Those are separate adapters: Workflow uses the active world implementation, and Sandbox uses the backend from `agent/sandbox` or `defaultBackend()`.
|
||||
|
||||
For advanced self-hosted deployments, the root `agent.ts` can select the installed Workflow world package to use with `experimental.workflow.world`:
|
||||
|
||||
@@ -65,8 +65,9 @@ export interface DurableSessionState {
|
||||
* `agent.compactionModelReference`, and the `compaction` thresholds —
|
||||
* those are rebuilt every turn from `bundle.turnAgent` by
|
||||
* {@link import("#execution/session.js").hydrateDurableSession}.
|
||||
* `agent.system` is the session-start prompt snapshot, pinned at
|
||||
* `createSession`.
|
||||
* `agent.system` is the last applied prompt snapshot. Before each model step,
|
||||
* the execution layer replaces it from the current deployment's
|
||||
* `bundle.turnAgent`.
|
||||
*/
|
||||
export interface DurableSession {
|
||||
readonly sessionId: string;
|
||||
|
||||
@@ -222,7 +222,7 @@ describe("mintSubagentContinuationToken", () => {
|
||||
});
|
||||
|
||||
describe("refreshSessionFromTurnAgent", () => {
|
||||
it("refreshes model/tool metadata while preserving history and system prompt", () => {
|
||||
it("refreshes the current agent configuration while preserving history", () => {
|
||||
const session = createSession({
|
||||
continuationToken: "root-token",
|
||||
sessionId: "sess-root",
|
||||
@@ -260,7 +260,7 @@ describe("refreshSessionFromTurnAgent", () => {
|
||||
contextWindowTokens: 200_000,
|
||||
id: "updated-model",
|
||||
});
|
||||
expect(refreshed.agent.system).toBe("You are a helpful assistant.\n\nBe concise.");
|
||||
expect(refreshed.agent.system).toBe("Completely different system prompt.");
|
||||
expect(refreshed.agent.tools).toEqual([
|
||||
{
|
||||
description: "Echoes text",
|
||||
@@ -311,7 +311,7 @@ describe("refreshSessionFromTurnAgent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("never changes the system prompt even when turnAgent instructions differ", () => {
|
||||
it("refreshes the system prompt when turnAgent instructions differ", () => {
|
||||
const session = createSession({
|
||||
continuationToken: "root-token",
|
||||
sessionId: "sess-root",
|
||||
@@ -322,7 +322,7 @@ describe("refreshSessionFromTurnAgent", () => {
|
||||
const refreshed = refreshSessionFromTurnAgent({
|
||||
session,
|
||||
turnAgent: createTestTurnAgent({
|
||||
instructions: ["Updated prompt that should be ignored."],
|
||||
instructions: ["Updated prompt from the current deployment."],
|
||||
model: { contextWindowTokens: 200_000, id: "updated-model" },
|
||||
}),
|
||||
});
|
||||
@@ -331,27 +331,6 @@ describe("refreshSessionFromTurnAgent", () => {
|
||||
contextWindowTokens: 200_000,
|
||||
id: "updated-model",
|
||||
});
|
||||
expect(refreshed.agent.system).toBe("Original session-start prompt.");
|
||||
});
|
||||
|
||||
it("refreshes the system prompt when explicitly requested", () => {
|
||||
const session = createSession({
|
||||
continuationToken: "root-token",
|
||||
sessionId: "sess-root",
|
||||
turnAgent: createTestTurnAgent({
|
||||
instructions: ["Original session-start prompt."],
|
||||
}),
|
||||
});
|
||||
const refreshed = refreshSessionFromTurnAgent({
|
||||
refreshSystemPrompt: true,
|
||||
session,
|
||||
turnAgent: createTestTurnAgent({
|
||||
instructions: ["Updated prompt from authored source.", "Updated tool context."],
|
||||
}),
|
||||
});
|
||||
|
||||
expect(refreshed.agent.system).toBe(
|
||||
"Updated prompt from authored source.\n\nUpdated tool context.",
|
||||
);
|
||||
expect(refreshed.agent.system).toBe("Updated prompt from the current deployment.");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,11 +55,7 @@ export interface CreateSessionInput {
|
||||
readonly outputSchema?: HarnessSession["outputSchema"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a fresh {@link HarnessSession}. The only site that derives
|
||||
* `session.agent.system` from a `turnAgent` — every subsequent turn
|
||||
* preserves the prompt via {@link refreshSessionFromTurnAgent}.
|
||||
*/
|
||||
/** Creates a fresh {@link HarnessSession} from the current `turnAgent`. */
|
||||
export function createSession(input: CreateSessionInput): HarnessSession {
|
||||
const { turnAgent } = input;
|
||||
const tools = createSessionToolDefinitions(turnAgent);
|
||||
@@ -93,15 +89,13 @@ export function createSession(input: CreateSessionInput): HarnessSession {
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes a session with the latest `turnAgent` — replaces model/tool
|
||||
* metadata and recalculates compaction thresholds; preserves history and
|
||||
* state. Production callers keep the session-start `agent.system` prompt,
|
||||
* while dev HMR callers can opt into refreshing it from authored source.
|
||||
* Refreshes a session with the latest `turnAgent` — replaces the system
|
||||
* prompt, model/tool metadata, and compaction thresholds while preserving
|
||||
* conversation history and state.
|
||||
*/
|
||||
export function refreshSessionFromTurnAgent(input: {
|
||||
readonly session: HarnessSession;
|
||||
readonly turnAgent: RuntimeTurnAgent;
|
||||
readonly refreshSystemPrompt?: boolean;
|
||||
readonly compactionOverrides?: {
|
||||
readonly thresholdPercent?: number;
|
||||
};
|
||||
@@ -111,10 +105,7 @@ export function refreshSessionFromTurnAgent(input: {
|
||||
agent: {
|
||||
compactionModelReference: input.turnAgent.compactionModel,
|
||||
modelReference: input.turnAgent.model,
|
||||
system:
|
||||
input.refreshSystemPrompt === true
|
||||
? input.turnAgent.instructions.join("\n\n")
|
||||
: input.session.agent.system,
|
||||
system: input.turnAgent.instructions.join("\n\n"),
|
||||
tools: createSessionToolDefinitions(input.turnAgent),
|
||||
},
|
||||
compaction: createCompactionConfig({
|
||||
|
||||
@@ -521,7 +521,7 @@ describe("turnStep", () => {
|
||||
expect(second.serializedContext[ThreadKey.name]).toBe("alpha");
|
||||
});
|
||||
|
||||
it("refreshes the system prompt for authored-source dev bundles", async () => {
|
||||
it("refreshes the system prompt from the current bundled deployment", async () => {
|
||||
const session = createStubSession({
|
||||
agent: {
|
||||
modelReference: { id: "test" },
|
||||
@@ -531,11 +531,7 @@ describe("turnStep", () => {
|
||||
});
|
||||
installSessionStoreMocks([session]);
|
||||
|
||||
const compiledArtifactsSource = {
|
||||
appRoot: "/tmp/eve-dev-agent",
|
||||
kind: "disk",
|
||||
moduleMapLoaderPath: "/tmp/eve-dev-agent/loader.ts",
|
||||
} as const;
|
||||
const compiledArtifactsSource = { kind: "bundled" } as const;
|
||||
const turnAgent = {
|
||||
...TestTurnAgent,
|
||||
instructions: ["Updated instructions.", "Updated runtime context."],
|
||||
@@ -588,6 +584,13 @@ describe("turnStep", () => {
|
||||
});
|
||||
|
||||
expect(observedSystemPrompt).toBe("Updated instructions.\n\nUpdated runtime context.");
|
||||
expect(createDurableSessionState).toHaveBeenLastCalledWith({
|
||||
session: expect.objectContaining({
|
||||
agent: expect.objectContaining({
|
||||
system: "Updated instructions.\n\nUpdated runtime context.",
|
||||
}),
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ import { buildTurnAttributes, readRootSessionId } from "#execution/eve-workflow-
|
||||
import { setEveAttributes } from "#runtime/attributes/emit.js";
|
||||
import { turnWorkflow } from "#execution/turn-workflow.js";
|
||||
import { createWorkflowRuntime, startWorkflowPreferLatest } from "#execution/workflow-runtime.js";
|
||||
import type { RuntimeCompiledArtifactsSource } from "#runtime/compiled-artifacts-source.js";
|
||||
|
||||
/**
|
||||
* Result of one durable harness step, consumed by the turn workflow.
|
||||
@@ -315,7 +314,6 @@ export async function turnStep(rawInput: TurnStepInput): Promise<DurableStepResu
|
||||
compactionOverrides: {
|
||||
thresholdPercent: bundle.resolvedAgent.config.compaction?.thresholdPercent,
|
||||
},
|
||||
refreshSystemPrompt: shouldRefreshSystemPromptFromTurnAgent(bundle.compiledArtifactsSource),
|
||||
session: lifecycleSession,
|
||||
turnAgent: bundle.turnAgent,
|
||||
});
|
||||
@@ -392,15 +390,6 @@ export async function turnStep(rawInput: TurnStepInput): Promise<DurableStepResu
|
||||
};
|
||||
}
|
||||
|
||||
function shouldRefreshSystemPromptFromTurnAgent(
|
||||
compiledArtifactsSource: RuntimeCompiledArtifactsSource,
|
||||
): boolean {
|
||||
return (
|
||||
compiledArtifactsSource.kind === "disk" &&
|
||||
compiledArtifactsSource.moduleMapLoaderPath !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the pending-state fields the turn workflow needs to choose
|
||||
* the right `NextDriverAction` arm at the park boundary.
|
||||
|
||||
Reference in New Issue
Block a user