fix: expose run step identities across transports
This commit is contained in:
@@ -4347,6 +4347,16 @@
|
|||||||
"state_changes": {
|
"state_changes": {
|
||||||
"$ref": "#/components/schemas/JsonObject"
|
"$ref": "#/components/schemas/JsonObject"
|
||||||
},
|
},
|
||||||
|
"step_number": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"step_type": {
|
"step_type": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -4355,6 +4365,7 @@
|
|||||||
"frame_id",
|
"frame_id",
|
||||||
"node_id",
|
"node_id",
|
||||||
"step_type",
|
"step_type",
|
||||||
|
"step_number",
|
||||||
"resolved_input",
|
"resolved_input",
|
||||||
"outcome",
|
"outcome",
|
||||||
"next_node_id",
|
"next_node_id",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class TraceEntryPayload(TypedDict):
|
|||||||
frame_id: str
|
frame_id: str
|
||||||
node_id: str
|
node_id: str
|
||||||
step_type: str
|
step_type: str
|
||||||
|
step_number: int | None
|
||||||
resolved_input: JsonObject
|
resolved_input: JsonObject
|
||||||
outcome: str
|
outcome: str
|
||||||
next_node_id: str
|
next_node_id: str
|
||||||
|
|||||||
@@ -438,6 +438,7 @@ class StartRunParams(RpcParamsModel):
|
|||||||
max_steps: int | None = Field(
|
max_steps: int | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
ge=1,
|
ge=1,
|
||||||
|
strict=True,
|
||||||
description=(
|
description=(
|
||||||
"Optional run step budget. The server default applies when omitted; "
|
"Optional run step budget. The server default applies when omitted; "
|
||||||
"resume never accepts a replacement."
|
"resume never accepts a replacement."
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ def test_decode_trace_result_decodes_bounded_trace() -> None:
|
|||||||
"frame_id": "root",
|
"frame_id": "root",
|
||||||
"node_id": "constant",
|
"node_id": "constant",
|
||||||
"step_type": "node",
|
"step_type": "node",
|
||||||
|
"step_number": 1,
|
||||||
"resolved_input": {},
|
"resolved_input": {},
|
||||||
"outcome": "ok",
|
"outcome": "ok",
|
||||||
"next_node_id": "__end__",
|
"next_node_id": "__end__",
|
||||||
@@ -176,3 +177,4 @@ def test_decode_trace_result_decodes_bounded_trace() -> None:
|
|||||||
assert result.trace_start == 0
|
assert result.trace_start == 0
|
||||||
assert result.trace is not None
|
assert result.trace is not None
|
||||||
assert result.trace[0]["node_id"] == "constant"
|
assert result.trace[0]["node_id"] == "constant"
|
||||||
|
assert result.trace[0]["step_number"] == 1
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ class _Port:
|
|||||||
"frame_id": "root",
|
"frame_id": "root",
|
||||||
"node_id": "approve",
|
"node_id": "approve",
|
||||||
"step_type": "node",
|
"step_type": "node",
|
||||||
|
"step_number": 1,
|
||||||
"resolved_input": {},
|
"resolved_input": {},
|
||||||
"outcome": "ok",
|
"outcome": "ok",
|
||||||
"next_node_id": "__end__",
|
"next_node_id": "__end__",
|
||||||
@@ -101,6 +102,7 @@ async def test_interrupted_run_resumes_and_reads_bounded_trace() -> None:
|
|||||||
assert trace.start == 0
|
assert trace.start == 0
|
||||||
assert trace.limit == 25
|
assert trace.limit == 25
|
||||||
assert len(trace.frames) == 1
|
assert len(trace.frames) == 1
|
||||||
|
assert trace.frames[0].step_number == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -1199,6 +1199,7 @@ async def test_rpc_runs_deployment_and_reads_bounded_trace(tmp_path) -> None:
|
|||||||
assert trace["result"]["trace_start"] == 0
|
assert trace["result"]["trace_start"] == 0
|
||||||
assert trace["result"]["trace_limit"] == 1
|
assert trace["result"]["trace_limit"] == 1
|
||||||
assert len(trace["result"]["trace"]) == 1
|
assert len(trace["result"]["trace"]) == 1
|
||||||
|
assert trace["result"]["trace"][0]["step_number"] == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_rpc_run_list_method(tmp_path) -> None:
|
async def test_rpc_run_list_method(tmp_path) -> None:
|
||||||
@@ -2309,6 +2310,28 @@ async def test_rpc_runs_start_rejects_non_positive_step_budget(tmp_path) -> None
|
|||||||
assert rejected["error"]["code"] == -32602
|
assert rejected["error"]["code"] == -32602
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("max_steps", [True, "5"])
|
||||||
|
async def test_rpc_runs_start_rejects_non_integer_step_budget(
|
||||||
|
tmp_path, max_steps: object
|
||||||
|
) -> None:
|
||||||
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
|
deployment_id = await _seed_step_budget_deployment(server)
|
||||||
|
app = create_rpc_app(server)
|
||||||
|
transport = httpx.ASGITransport(app=app)
|
||||||
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
rejected = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.runs.start",
|
||||||
|
{
|
||||||
|
"deployment_id": deployment_id,
|
||||||
|
"workflow_input": {},
|
||||||
|
"max_steps": max_steps,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert rejected["error"]["code"] == -32602
|
||||||
|
|
||||||
|
|
||||||
async def test_rpc_runs_resume_rejects_step_budget_replacement(tmp_path) -> None:
|
async def test_rpc_runs_resume_rejects_step_budget_replacement(tmp_path) -> None:
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
app = create_rpc_app(server)
|
app = create_rpc_app(server)
|
||||||
|
|||||||
@@ -2481,6 +2481,7 @@ export interface TraceEntryPayload {
|
|||||||
output: JsonObject;
|
output: JsonObject;
|
||||||
resolved_input: JsonObject;
|
resolved_input: JsonObject;
|
||||||
state_changes: JsonObject;
|
state_changes: JsonObject;
|
||||||
|
step_number: number | null;
|
||||||
step_type: string;
|
step_type: string;
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
}
|
}
|
||||||
@@ -5018,6 +5019,16 @@ export const workflowRuntimeContract = {
|
|||||||
"state_changes": {
|
"state_changes": {
|
||||||
"$ref": "#/components/schemas/JsonObject"
|
"$ref": "#/components/schemas/JsonObject"
|
||||||
},
|
},
|
||||||
|
"step_number": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"step_type": {
|
"step_type": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -5026,6 +5037,7 @@ export const workflowRuntimeContract = {
|
|||||||
"frame_id",
|
"frame_id",
|
||||||
"node_id",
|
"node_id",
|
||||||
"step_type",
|
"step_type",
|
||||||
|
"step_number",
|
||||||
"resolved_input",
|
"resolved_input",
|
||||||
"outcome",
|
"outcome",
|
||||||
"next_node_id",
|
"next_node_id",
|
||||||
|
|||||||
@@ -301,11 +301,15 @@ const RunResultSchema = Schema.Struct({
|
|||||||
output: Schema.NullOr(JsonObjectSchema),
|
output: Schema.NullOr(JsonObjectSchema),
|
||||||
diagnostics: Schema.Array(Schema.Unknown),
|
diagnostics: Schema.Array(Schema.Unknown),
|
||||||
trace_count: NonNegativeIntegerSchema,
|
trace_count: NonNegativeIntegerSchema,
|
||||||
|
max_steps: PositiveIntegerSchema,
|
||||||
|
steps_executed: NonNegativeIntegerSchema,
|
||||||
|
steps_remaining: NonNegativeIntegerSchema,
|
||||||
next_actions: RunNextActionsSchema,
|
next_actions: RunNextActionsSchema,
|
||||||
});
|
});
|
||||||
const TraceFrameSchema = Schema.Struct({
|
const TraceFrameSchema = Schema.Struct({
|
||||||
node_id: Schema.String,
|
node_id: Schema.String,
|
||||||
step_type: Schema.String,
|
step_type: Schema.String,
|
||||||
|
step_number: Schema.NullOr(PositiveIntegerSchema),
|
||||||
resolved_input: JsonObjectSchema,
|
resolved_input: JsonObjectSchema,
|
||||||
outcome: Schema.String,
|
outcome: Schema.String,
|
||||||
output: JsonObjectSchema,
|
output: JsonObjectSchema,
|
||||||
@@ -764,6 +768,7 @@ export const authoredRpcSchemas = {
|
|||||||
deployment_id: Schema.String,
|
deployment_id: Schema.String,
|
||||||
workflow_input: JsonObjectSchema,
|
workflow_input: JsonObjectSchema,
|
||||||
trace_range: Schema.optional(Schema.NullOr(TraceRangeSchema)),
|
trace_range: Schema.optional(Schema.NullOr(TraceRangeSchema)),
|
||||||
|
max_steps: Schema.optional(PositiveIntegerSchema),
|
||||||
}),
|
}),
|
||||||
success: RunResultSchema,
|
success: RunResultSchema,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -141,6 +141,9 @@ const interruptedRun = {
|
|||||||
output: null,
|
output: null,
|
||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
trace_count: 1,
|
trace_count: 1,
|
||||||
|
max_steps: 10_000,
|
||||||
|
steps_executed: 1,
|
||||||
|
steps_remaining: 9_999,
|
||||||
next_actions: nextActions,
|
next_actions: nextActions,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -173,11 +176,14 @@ const completedRun = {
|
|||||||
outcome: "completed",
|
outcome: "completed",
|
||||||
output: { report: "# Completed report" },
|
output: { report: "# Completed report" },
|
||||||
trace_count: 4,
|
trace_count: 4,
|
||||||
|
steps_executed: 4,
|
||||||
|
steps_remaining: 9_996,
|
||||||
};
|
};
|
||||||
|
|
||||||
const authoredTraceFrame = {
|
const authoredTraceFrame = {
|
||||||
node_id: "review_issues",
|
node_id: "review_issues",
|
||||||
step_type: "interrupt",
|
step_type: "interrupt",
|
||||||
|
step_number: 1,
|
||||||
resolved_input: { report: "# Draft report" },
|
resolved_input: { report: "# Draft report" },
|
||||||
outcome: "submitted",
|
outcome: "submitted",
|
||||||
output: {},
|
output: {},
|
||||||
|
|||||||
@@ -315,6 +315,9 @@ describe("runtimeSchemasFor", () => {
|
|||||||
output: null,
|
output: null,
|
||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
trace_count: 1,
|
trace_count: 1,
|
||||||
|
max_steps: 10_000,
|
||||||
|
steps_executed: 1,
|
||||||
|
steps_remaining: 9_999,
|
||||||
next_actions: {
|
next_actions: {
|
||||||
can_continue: true,
|
can_continue: true,
|
||||||
can_save_now: null,
|
can_save_now: null,
|
||||||
@@ -381,6 +384,7 @@ describe("runtimeSchemasFor", () => {
|
|||||||
frame_id: "frame_1",
|
frame_id: "frame_1",
|
||||||
node_id: "review_issues",
|
node_id: "review_issues",
|
||||||
step_type: "interrupt",
|
step_type: "interrupt",
|
||||||
|
step_number: 1,
|
||||||
resolved_input: {},
|
resolved_input: {},
|
||||||
outcome: "submitted",
|
outcome: "submitted",
|
||||||
next_node_id: "create_issues",
|
next_node_id: "create_issues",
|
||||||
@@ -401,6 +405,9 @@ describe("runtimeSchemasFor", () => {
|
|||||||
output: {},
|
output: {},
|
||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
trace_count: 1,
|
trace_count: 1,
|
||||||
|
max_steps: 10_000,
|
||||||
|
steps_executed: 1,
|
||||||
|
steps_remaining: 9_999,
|
||||||
next_actions: {
|
next_actions: {
|
||||||
can_continue: false,
|
can_continue: false,
|
||||||
can_save_now: null,
|
can_save_now: null,
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ describe("checked workflow manifest translation", () => {
|
|||||||
run_id: "run_123",
|
run_id: "run_123",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
trace_count: 9,
|
trace_count: 9,
|
||||||
|
max_steps: 10_000,
|
||||||
|
steps_executed: 9,
|
||||||
|
steps_remaining: 9_991,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
|||||||
@@ -389,6 +389,9 @@ const interpretRunDetail = (decoded: {
|
|||||||
readonly output: Record<string, unknown> | null;
|
readonly output: Record<string, unknown> | null;
|
||||||
readonly diagnostics: ReadonlyArray<unknown>;
|
readonly diagnostics: ReadonlyArray<unknown>;
|
||||||
readonly trace_count: number;
|
readonly trace_count: number;
|
||||||
|
readonly max_steps: number;
|
||||||
|
readonly steps_executed: number;
|
||||||
|
readonly steps_remaining: number;
|
||||||
readonly next_actions: Parameters<typeof interpretNextActions>[0];
|
readonly next_actions: Parameters<typeof interpretNextActions>[0];
|
||||||
}) => ({
|
}) => ({
|
||||||
runId: decoded.run_id,
|
runId: decoded.run_id,
|
||||||
@@ -403,6 +406,9 @@ const interpretRunDetail = (decoded: {
|
|||||||
output: decoded.output,
|
output: decoded.output,
|
||||||
diagnostics: decoded.diagnostics,
|
diagnostics: decoded.diagnostics,
|
||||||
traceCount: decoded.trace_count,
|
traceCount: decoded.trace_count,
|
||||||
|
maxSteps: decoded.max_steps,
|
||||||
|
stepsExecuted: decoded.steps_executed,
|
||||||
|
stepsRemaining: decoded.steps_remaining,
|
||||||
nextActions: interpretNextActions(decoded.next_actions),
|
nextActions: interpretNextActions(decoded.next_actions),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1268,6 +1274,7 @@ const operationEntries = defineOperationEntries([
|
|||||||
frameId: entry.frame_id,
|
frameId: entry.frame_id,
|
||||||
nodeId: entry.node_id,
|
nodeId: entry.node_id,
|
||||||
stepType: entry.step_type,
|
stepType: entry.step_type,
|
||||||
|
stepNumber: entry.step_number,
|
||||||
outcome: entry.outcome,
|
outcome: entry.outcome,
|
||||||
nextNodeId: entry.next_node_id,
|
nextNodeId: entry.next_node_id,
|
||||||
resolvedInput: entry.resolved_input,
|
resolvedInput: entry.resolved_input,
|
||||||
|
|||||||
@@ -483,6 +483,9 @@ const lifecycleCases = [
|
|||||||
output: null,
|
output: null,
|
||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
trace_count: 0,
|
trace_count: 0,
|
||||||
|
max_steps: 10_000,
|
||||||
|
steps_executed: 1,
|
||||||
|
steps_remaining: 9_999,
|
||||||
next_actions: {
|
next_actions: {
|
||||||
can_continue: false,
|
can_continue: false,
|
||||||
can_save_now: null,
|
can_save_now: null,
|
||||||
@@ -550,6 +553,9 @@ const lifecycleCases = [
|
|||||||
output: null,
|
output: null,
|
||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
trace_count: 1,
|
trace_count: 1,
|
||||||
|
max_steps: 10_000,
|
||||||
|
steps_executed: 1,
|
||||||
|
steps_remaining: 9_999,
|
||||||
next_actions: {
|
next_actions: {
|
||||||
can_continue: true,
|
can_continue: true,
|
||||||
can_save_now: null,
|
can_save_now: null,
|
||||||
@@ -597,6 +603,9 @@ const lifecycleCases = [
|
|||||||
},
|
},
|
||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
trace_count: 4,
|
trace_count: 4,
|
||||||
|
max_steps: 10_000,
|
||||||
|
steps_executed: 4,
|
||||||
|
steps_remaining: 9_996,
|
||||||
next_actions: {
|
next_actions: {
|
||||||
can_continue: false,
|
can_continue: false,
|
||||||
can_save_now: null,
|
can_save_now: null,
|
||||||
@@ -635,6 +644,9 @@ const lifecycleCases = [
|
|||||||
output: null,
|
output: null,
|
||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
trace_count: 1,
|
trace_count: 1,
|
||||||
|
max_steps: 10_000,
|
||||||
|
steps_executed: 1,
|
||||||
|
steps_remaining: 9_999,
|
||||||
next_actions: {
|
next_actions: {
|
||||||
can_continue: true,
|
can_continue: true,
|
||||||
can_save_now: null,
|
can_save_now: null,
|
||||||
@@ -651,6 +663,7 @@ const lifecycleCases = [
|
|||||||
frame_id: "frame_1",
|
frame_id: "frame_1",
|
||||||
node_id: "review",
|
node_id: "review",
|
||||||
step_type: "interrupt",
|
step_type: "interrupt",
|
||||||
|
step_number: 1,
|
||||||
resolved_input: { report: "..." },
|
resolved_input: { report: "..." },
|
||||||
outcome: "submitted",
|
outcome: "submitted",
|
||||||
output: {},
|
output: {},
|
||||||
@@ -958,6 +971,7 @@ describe("lifecycle operations", () => {
|
|||||||
frameId: "frame_1",
|
frameId: "frame_1",
|
||||||
nodeId: "review",
|
nodeId: "review",
|
||||||
stepType: "interrupt",
|
stepType: "interrupt",
|
||||||
|
stepNumber: 1,
|
||||||
outcome: "submitted",
|
outcome: "submitted",
|
||||||
nextNodeId: "create_issues",
|
nextNodeId: "create_issues",
|
||||||
},
|
},
|
||||||
@@ -1019,6 +1033,9 @@ describe("lifecycle operations", () => {
|
|||||||
expect(exchange.interpreted).toMatchObject({
|
expect(exchange.interpreted).toMatchObject({
|
||||||
runId: "run_demo",
|
runId: "run_demo",
|
||||||
status: "interrupted",
|
status: "interrupted",
|
||||||
|
maxSteps: 10_000,
|
||||||
|
stepsExecuted: 1,
|
||||||
|
stepsRemaining: 9_999,
|
||||||
interrupt: {
|
interrupt: {
|
||||||
kind: "issue_review",
|
kind: "issue_review",
|
||||||
typed: true,
|
typed: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user