Skip to content
Merged
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
4 changes: 4 additions & 0 deletions apps/agent-worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ the Daytona adapter normalizes the provider's structured `503` response to an in
then the lifecycle verifies the active-run lease and canonical volume mount before replacing the
stopped container on the same isolated workspace-volume subpath. This preserves user files while
avoiding an indefinite dependency on one unhealthy runner.
Provider output-length termination is nonterminal: the Workflow checkpoints the partial model turn,
adds an internal continuation message, and resumes from that durable state. A tool-free turn completes
the run only when the model reports an actual stop; content filtering, provider errors, and invalid
terminal reasons fail explicitly instead of publishing incomplete work as success.
There is no application step, token, duration, or cost ceiling; semantic completion ends the loop,
while per-operation timeouts and the platform Workflow limit remain operational safeguards.
The Worker pins Cloudflare's paid-plan maximum subrequest allowance because external provider,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
executeGeneralAgentTool,
GeneralAgentFinishReasonSchema,
type GeneralAgentToolCall,
generateGeneralAgentStep,
} from "@cheatcode/agent-core";
Expand Down Expand Up @@ -87,7 +88,7 @@ export const WorkflowModelStepResultSchema = z.strictObject({
input: StartRunInputSchema,
logicalModelId: LogicalModelIdSchema,
step: z.strictObject({
finishReason: z.string(),
finishReason: GeneralAgentFinishReasonSchema,
responseMessages: z.array(WorkflowJsonValueSchema),
text: z.string(),
toolCalls: z.array(WorkflowToolCallSchema),
Expand Down
37 changes: 37 additions & 0 deletions apps/agent-worker/src/durable-objects/agent-run-workflow.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from "cloudflare:workers";
import { NonRetryableError } from "cloudflare:workflows";
import {
APIError,
createLogger,
emitErrorEvent,
emitUserEvent,
Expand Down Expand Up @@ -57,6 +58,20 @@ const STATE_STEP = stepConfig("2 minutes", 5);
const CLEANUP_STEP = stepConfig("5 minutes", 5);
const FAILURE_STEP = stepConfig("2 minutes", AGENT_RUN_WORKFLOW_FAILURE_RETRY_LIMIT);
const JsonValueSchema = z.json();
const MODEL_LENGTH_CONTINUATION_MESSAGE: ModelMessage = {
role: "user",
content: [
{
type: "text",
text: [
"The provider ended your previous turn at its per-response output limit.",
"Continue the unfinished work from the current state without repeating completed work.",
"If a tool call was interrupted, split the remaining operation into smaller complete tool calls.",
"Do not claim completion until the requested outcome has been verified.",
].join(" "),
},
],
};

interface AgentRunWorkflowEnv extends AgentRunEnv, AgentRunWorkflowBindings {
AGENT_RUN: DurableObjectNamespace<AgentRun>;
Expand Down Expand Up @@ -185,6 +200,11 @@ async function runAgentLoop(
stepIndex,
);
if (model.step.toolCalls.length === 0) {
if (model.step.finishReason === "length") {
state = continueTruncatedModelTurn(state);
continue;
}
requireSemanticCompletion(model.step.finishReason);
await publishClosingBackstopIfNeeded(env, workflowStep, workflowInstanceId, payload, state);
return;
}
Expand Down Expand Up @@ -219,6 +239,23 @@ async function runAgentLoop(
}
}

function continueTruncatedModelTurn(state: WorkflowAgentState): WorkflowAgentState {
return WorkflowAgentStateSchema.parse({
...state,
messages: [...state.messages, workflowJsonValue(MODEL_LENGTH_CONTINUATION_MESSAGE)],
});
}

function requireSemanticCompletion(
finishReason: WorkflowModelStepResult["step"]["finishReason"],
): void {
if (finishReason === "stop") return;
throw new APIError(502, "upstream_llm_failed", "The model stopped before completing the run.", {
details: { finishReason },
retriable: finishReason !== "content-filter",
});
}

async function executeModelWorkflowStep(input: {
env: AgentRunWorkflowEnv;
payload: AgentRunWorkflowPayload;
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Mastra agents, tool registry, and workflow entrypoints.
- `createCodeRequestContext`
- `generateGeneralAgentStep` and `executeGeneralAgentTool` for Cloudflare
Workflow-owned, step-granular agent execution
- `GeneralAgentFinishReasonSchema`, the validated model-turn completion contract
- runtime credential and model contracts consumed by `agent-worker`

The tool and agent registries are statically constrained by the lightweight
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type {
} from "./mastra/composio-context";
export {
executeGeneralAgentTool,
GeneralAgentFinishReasonSchema,
type GeneralAgentToolCall,
generateGeneralAgentStep,
} from "./mastra/durable-agent-step";
Expand Down
15 changes: 13 additions & 2 deletions packages/agent-core/src/mastra/durable-agent-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,25 @@ import { z } from "zod";
import { mastra } from "./index";
import { cheatcodeTools } from "./tool-defs/tool-set";

export const GeneralAgentFinishReasonSchema = z.enum([
"stop",
"length",
"content-filter",
"tool-calls",
"error",
"other",
]);

type GeneralAgentFinishReason = z.infer<typeof GeneralAgentFinishReasonSchema>;

export interface GeneralAgentToolCall {
input: JSONValue;
toolCallId: string;
toolName: string;
}

interface GeneralAgentStepResult {
finishReason: string;
finishReason: GeneralAgentFinishReason;
responseMessages: JSONValue[];
text: string;
toolCalls: GeneralAgentToolCall[];
Expand Down Expand Up @@ -51,7 +62,7 @@ export async function generateGeneralAgentStep(
runId: options.runId,
});
return {
finishReason: result.finishReason ?? "unknown",
finishReason: GeneralAgentFinishReasonSchema.parse(result.finishReason),
responseMessages: toJsonValues(result.response.messages ?? []),
text: result.text,
toolCalls: result.toolCalls.map((call) => ({
Expand Down