diff --git a/apps/fabro-web/app/components/event-debug-helpers.tsx b/apps/fabro-web/app/components/event-debug-helpers.tsx index 1a55035bd..c32fc1d61 100644 --- a/apps/fabro-web/app/components/event-debug-helpers.tsx +++ b/apps/fabro-web/app/components/event-debug-helpers.tsx @@ -5,7 +5,9 @@ export type DebugCategory = | "command" | "lifecycle" | "human" - | "system"; + | "system" + | "petri" + | "platform"; export const DEBUG_CATEGORIES: readonly DebugCategory[] = [ "agent", @@ -13,6 +15,8 @@ export const DEBUG_CATEGORIES: readonly DebugCategory[] = [ "lifecycle", "human", "system", + "petri", + "platform", ] as const; const PREFIX_TO_CATEGORY: Record = { @@ -34,6 +38,8 @@ const CATEGORY_LABEL: Record = { lifecycle: "Lifecycle", human: "Human", system: "System", + petri: "Petri", + platform: "Platform", }; const CATEGORY_TONE: Record = { @@ -42,6 +48,8 @@ const CATEGORY_TONE: Record = { lifecycle: "bg-amber/15 text-amber", human: "bg-coral/15 text-coral", system: "bg-overlay-strong text-fg-3", + petri: "bg-teal-500/15 text-teal-500", + platform: "bg-amber/15 text-amber", }; const CATEGORY_COLOR: Record = { @@ -50,6 +58,8 @@ const CATEGORY_COLOR: Record = { lifecycle: "var(--color-amber)", human: "var(--color-coral)", system: "var(--color-ice-300)", + petri: "var(--color-teal-500)", + platform: "var(--color-amber)", }; export function debugCategory(eventName: string | null | undefined): DebugCategory { diff --git a/apps/fabro-web/app/components/event-debug.tsx b/apps/fabro-web/app/components/event-debug.tsx index b50ced2f9..e23fd59d3 100644 --- a/apps/fabro-web/app/components/event-debug.tsx +++ b/apps/fabro-web/app/components/event-debug.tsx @@ -12,7 +12,6 @@ import { FunnelIcon, MagnifyingGlassIcon, } from "@heroicons/react/16/solid"; -import type { EventEnvelope } from "@qltysh/fabro-api-client"; import { Tooltip } from "./ui"; import { formatAbsoluteTs } from "../lib/format"; @@ -28,19 +27,36 @@ import { import { FloatingTooltip } from "./floating-tooltip"; import { useWindowEvent } from "../hooks/effects"; +/** + * What a debug row needs of an event: a legacy `EventEnvelope`, or a Petri + * run stream item as `debugRowsFromStream` shapes it (with its own + * category, since Petri's `.` names map to none of the + * legacy prefixes). + */ +export interface DebugRowLike { + seq: number; + event?: string | null; + ts: string; + category?: DebugCategory; +} + +export function debugRowCategory(row: DebugRowLike): DebugCategory { + return row.category ?? debugCategory(row.event); +} + export function DebugEventRow({ event, runStart, selected, onSelect, }: { - event: EventEnvelope; + event: DebugRowLike; runStart: string | undefined; selected: boolean; onSelect: () => void; }) { const eventName = event.event ?? ""; - const category = debugCategory(eventName); + const category = debugRowCategory(event); return ( + )} + + {all.length > 0 && ( + + {isFiltering + ? `${filtered.length.toLocaleString()} of ${all.length.toLocaleString()} items` + : `${all.length.toLocaleString()} items`} + + )} + + + +
+ {all.length === 0 ? ( +
+ +
+ ) : filtered.length === 0 ? ( +
+ No events match these filters. +
+ ) : ( + filtered.map((row) => ( + setOpenSeq(row.seq)} + /> + )) + )} +
+ + + setOpenSeq(null)} /> + + ); +} + +/** A debug row with the stage the item belongs to beside its name. */ +function StreamEventRow({ + row, + runStart, + selected, + onSelect, +}: { + row: DebugRow; + runStart: string | undefined; + selected: boolean; + onSelect: () => void; +}) { + return ( +
+ + {row.stageLabel && ( + + {row.stageLabel} + + )} +
+ ); +} + function errorMessage(error: unknown): string | undefined { return error instanceof Error ? error.message : undefined; } diff --git a/apps/fabro-web/app/routes/run-overview.test.tsx b/apps/fabro-web/app/routes/run-overview.test.tsx index ae5f159b9..cda806718 100644 --- a/apps/fabro-web/app/routes/run-overview.test.tsx +++ b/apps/fabro-web/app/routes/run-overview.test.tsx @@ -22,6 +22,8 @@ mock.module("../lib/queries", () => ({ }), useRunGraphSource: () => ({ data: undefined }), useRunStageEvents: () => ({ data: [] }), + useRunState: () => ({ data: undefined }), + useRunStream: () => ({ data: undefined }), })); mock.module("../components/run-summary-panel", () => ({ diff --git a/apps/fabro-web/app/routes/run-overview.tsx b/apps/fabro-web/app/routes/run-overview.tsx index 0150d19ea..3a6102145 100644 --- a/apps/fabro-web/app/routes/run-overview.tsx +++ b/apps/fabro-web/app/routes/run-overview.tsx @@ -3,6 +3,7 @@ import { useNavigate, useParams } from "react-router"; import { ApiError } from "../lib/api-client"; import { useRun, useRunGraph, useRunGraphSource, useRunStages } from "../lib/queries"; import { FloatingTooltip } from "../components/floating-tooltip"; +import { PlatformRecordsPanel } from "../components/platform-records-panel"; import { RunSummaryPanel } from "../components/run-summary-panel"; import { StagePopover } from "../components/stage-popover"; import { StageSidebar } from "../components/stage-sidebar"; @@ -150,8 +151,9 @@ export default function RunOverview() {
-
+
+
{graphSvg === undefined && graphQuery.isLoading ? (
diff --git a/apps/fabro-web/app/routes/run-petri.render.test.tsx b/apps/fabro-web/app/routes/run-petri.render.test.tsx new file mode 100644 index 000000000..5a98439d1 --- /dev/null +++ b/apps/fabro-web/app/routes/run-petri.render.test.tsx @@ -0,0 +1,286 @@ +/** + * The run detail's views over a Petri run, rendered from the projection and + * the stream the server tests captured (`test-fixtures/petri/*.json`): one + * scenario per fixture, every view `VIEWS.md` lists that the web app draws + * from those two sources. + */ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import type { ReactElement } from "react"; +import type { RunStage } from "@qltysh/fabro-api-client"; +import TestRenderer, { act } from "react-test-renderer"; +import { MemoryRouter } from "react-router"; + +import { PlatformRecordsPanelView } from "../components/platform-records-panel"; +import { RunWaterfall } from "../components/run-waterfall"; +import { StageSidebar } from "../components/stage-sidebar"; +import { FanInResults } from "../components/stage-renderers/fan-in-results"; +import { HumanQA } from "../components/stage-renderers/human-qa"; +import { ParallelChildren } from "../components/stage-renderers/parallel-children"; +import { ConditionalDecision } from "../components/stage-renderers/conditional-decision"; +import { loadPetriFixture, type PetriFixture } from "../lib/petri-fixtures"; +import { + debugRowsFromStream, + deriveRunPhasesFromStream, + findPetriEdgeForStage, + itemsForStage, + parallelOverviewFromProjection, + parsePetriInterviewPairs, + platformRecordsOf, + reducerTranscriptFromProjection, + stagesFromProjection, +} from "../lib/petri-stream"; +import { setupReactTestEnv } from "../lib/test-utils"; +import { StreamEventsView } from "./run-events"; +import { StageChatView, buildPetriStageActivity } from "./run-stages"; + +let teardown: () => void; +beforeEach(() => { + teardown = setupReactTestEnv(); +}); +afterEach(() => teardown()); + +function render(element: ReactElement): string { + let renderer!: TestRenderer.ReactTestRenderer; + act(() => { + renderer = TestRenderer.create( + {element}, + ); + }); + const json = JSON.stringify(renderer.toJSON()); + act(() => renderer.unmount()); + return json; +} + +function runStages(fixture: PetriFixture): RunStage[] { + return stagesFromProjection(fixture.projection).map((stage) => ({ + id: stage.id, + name: stage.name, + handler: stage.handler, + status: stage.status, + node_id: stage.nodeId, + visit: stage.visit, + started_at: stage.startedAt, + wall_time_ms: fixture.projection.stages[stage.id]?.timing?.wall_time_ms, + usage: stage.usage, + parallel_group_id: stage.parallelGroupId ?? undefined, + parallel_branch_index: stage.parallelBranchIndex ?? undefined, + })); +} + +function createdAt(fixture: PetriFixture): string { + return new Date(fixture.stream[0].recorded_at).toISOString(); +} + +describe("a command-only run", () => { + const fixture = loadPetriFixture("command"); + const stages = stagesFromProjection(fixture.projection); + + test("the stage list shows every stage with its state", () => { + expect(stages.map((stage) => [stage.id, stage.status])).toEqual([ + ["start@1", "succeeded"], + ["say@1", "succeeded"], + ["exit@1", "succeeded"], + ]); + const html = render(); + for (const name of ["start", "say", "exit"]) expect(html).toContain(name); + // The sidebar shows a stage's state as its icon's tone: mint is succeeded. + expect((html.match(/text-mint/g) ?? []).length).toBe(3); + expect(html).not.toContain("animate-pulse"); + }); + + test("the command stage is one command turn with its exit status and output size", () => { + const say = fixture.projection.stages["say@1"]; + const activity = buildPetriStageActivity( + itemsForStage(fixture.stream, "say@1"), + say, + "command", + ); + expect(activity.turns).toHaveLength(1); + expect(activity.turns[0]).toMatchObject({ + kind: "command", + running: false, + exitCode: 0, + outputBytes: say.output_bytes, + }); + }); + + test("the waterfall's phases come from the lifecycle records", () => { + const html = render( + , + ); + for (const label of ["Submitted", "Runnable", "Initializing", "say"]) { + expect(html).toContain(label); + } + }); +}); + +describe("the hello run on the twin", () => { + const fixture = loadPetriFixture("hello"); + const stages = stagesFromProjection(fixture.projection); + + test("the agent stage's chat shows the prompt and the agent's response", () => { + const greet = stages.find((stage) => stage.id === "greet@1")!; + expect(greet.handler).toBe("agent"); + const activity = buildPetriStageActivity( + itemsForStage(fixture.stream, "greet@1"), + fixture.projection.stages["greet@1"], + "agent", + ); + expect(activity.turns.map((turn) => turn.kind)).toEqual(["system", "assistant"]); + expect(activity.turns[0]).toMatchObject({ kind: "system" }); + expect((activity.turns[0] as { content: string }).content).toContain("Add a haiku"); + expect(activity.turns[1]).toMatchObject({ + kind: "assistant", + content: "A haiku, added.", + inputTokens: 1, + outputTokens: 5, + }); + const html = render( + , + ); + expect(html).toContain("A haiku, added."); + }); + + test("the stage list shows the agent stage succeeded", () => { + const greet = stages.find((stage) => stage.id === "greet@1")!; + expect(greet.status).toBe("succeeded"); + expect(greet.providerUsed?.model).toBe("gpt-5.4"); + const html = render(); + expect(html).toContain("greet"); + expect(html).toContain("text-mint"); + }); +}); + +describe("a two-branch parallel run", () => { + const fixture = loadPetriFixture("parallel"); + const stages = stagesFromProjection(fixture.projection); + + test("the fork lists both branches under it with their outcomes", () => { + const fork = stages.find((stage) => stage.id === "fork@1")!; + const html = render( + , + ); + expect(html).toContain("Branches"); + expect(html).toContain("/runs/run-1/stages/a@1"); + expect(html).toContain("/runs/run-1/stages/b@1"); + expect((html.match(/Succeeded/g) ?? []).length).toBeGreaterThanOrEqual(2); + }); + + test("the fan-in joined the branches", () => { + const merge = stages.find((stage) => stage.id === "merge@1")!; + const html = render( + , + ); + expect(html).toContain("Joined"); + expect(html).not.toContain("Reducer transcript"); + }); + + test("the events view lists Petri events by name and the platform notice", () => { + const html = render( + {}} + runStart={createdAt(fixture)} + view="events" + onChangeView={() => {}} + />, + ); + for (const name of ["run.started", "visit.started", "fork.completed", "run.finished"]) { + expect(html).toContain(name); + } + expect(html).toContain("run.notice"); + expect(html).toContain('"data-stage":"a@1"'); + expect(html).toContain(`${fixture.stream.length} items`); + }); + + test("the overview lists the platform records", () => { + const html = render( + , + ); + expect(html).toContain("Platform records"); + expect(html).toContain("Notice"); + expect(html).toContain("recorded while both branches ran"); + // The checkpoint hook wrote one record per stage, with its commit. + expect(html).toContain("Checkpoint"); + const checkpoint = fixture.stream.find( + (item) => item.kind === "platform" && item.item.record?.kind === "checkpoint", + ); + expect(checkpoint).toBeDefined(); + expect(html).toContain(String(checkpoint!.item.record.git_commit_sha).slice(0, 12)); + }); + + test("the sidebar groups the branches under the fork", () => { + const branches = stages.filter((stage) => stage.parallelGroupId === "fork@1"); + expect(branches.map((stage) => stage.id)).toEqual(["a@1", "b@1"]); + const html = render(); + for (const name of ["fork", "a", "b", "merge"]) expect(html).toContain(name); + }); +}); + +describe("a human gate answered through the API", () => { + const fixture = loadPetriFixture("gate"); + const stages = stagesFromProjection(fixture.projection); + + test("the Q&A shows the question, its options and the answer with who gave it", () => { + const gate = stages.find((stage) => stage.id === "gate@1")!; + expect(gate.handler).toBe("human"); + const html = render( + , + ); + expect(html).toContain("Go?"); + expect(html).toContain("[Y] Yes"); + expect(html).toContain("[N] No"); + expect(html).toContain("dev"); + expect(html).not.toContain("pending"); + }); + + test("the gate's decision took the no edge", () => { + const gate = stages.find((stage) => stage.id === "gate@1")!; + const edge = findPetriEdgeForStage(fixture.stream, "gate@1"); + const html = render( + , + ); + expect(html).toContain("/runs/run-1/stages/no@1"); + expect(html).not.toContain("No target"); + }); + + test("no question is left pending in the projection", () => { + expect(Object.keys(fixture.projection.pending_interviews)).toEqual([]); + expect(stages.map((stage) => stage.id)).toEqual(["start@1", "gate@1", "no@1", "exit@1"]); + }); +}); diff --git a/apps/fabro-web/app/routes/run-stages.tsx b/apps/fabro-web/app/routes/run-stages.tsx index 1613096e8..06c802053 100644 --- a/apps/fabro-web/app/routes/run-stages.tsx +++ b/apps/fabro-web/app/routes/run-stages.tsx @@ -23,6 +23,7 @@ import { EventSearchInput, MultiSelectFilter, ThreadDnaStrip, + debugRowCategory, threadSelectionId, threadSelectionsEqual, } from "../components/event-debug"; @@ -33,6 +34,7 @@ import { type DebugCategory, } from "../components/event-debug-helpers"; import type { + EventDisplayPayload, ThreadDnaItem, ThreadDnaSelection, } from "../components/event-debug"; @@ -51,7 +53,13 @@ import { } from "../components/ui"; import { ConditionalDecision } from "../components/stage-renderers/conditional-decision"; import { FanInResults } from "../components/stage-renderers/fan-in-results"; -import { extractStageContext } from "../components/stage-renderers/helpers"; +import { + extractStageContext, + type EdgeSelection, + type HumanInterviewPair, + type ParallelOverview, + type ReducerTranscript, +} from "../components/stage-renderers/helpers"; import { HumanQA } from "../components/stage-renderers/human-qa"; import { ParallelChildren } from "../components/stage-renderers/parallel-children"; import { @@ -71,6 +79,21 @@ import { } from "../lib/format"; import { costSourceTag, hasUsage, usageTokenBuckets } from "../lib/usage"; import { plural } from "../lib/plural"; +import { + agentEnvelopesOf, + commandOutcomeOf, + commandScriptOf, + debugRowSearchText, + debugRowsFromStream, + extractPetriStageContext, + findPetriEdgeForStage, + isPetriRun, + itemsForStage, + parallelOverviewFromProjection, + parsePetriInterviewPairs, + reducerTranscriptFromProjection, + type DebugRow, +} from "../lib/petri-stream"; import { useRun, useRunEventsList, @@ -79,6 +102,7 @@ import { useRunStageLog, useRunStages, useRunState, + useRunStream, } from "../lib/queries"; import { STAGE_ACTIVITY_EVENT_TYPES, @@ -98,6 +122,9 @@ import { import type { EventEnvelope, ReasoningOutput, + RunProjection, + RunStreamItem, + StageProjection, StageHandler, StageModelUsage, Usage, @@ -507,6 +534,177 @@ export function eventsToActivity( return buildStageActivity(events, stageId).turns; } +/** The stream and projection of a Petri run, threaded into the stage views. */ +export interface PetriRunData { + stream: RunStreamItem[]; + projection: RunProjection; +} + +/** + * The turns of a stage that ran on Petri: the prompt the projection holds, + * the Pebble envelopes the stage's step recorded (assistant messages, tool + * calls, interrupts), and, when no envelope carried the answer, the + * projection's response as the one assistant turn. A command stage is one + * command turn from its `step.started` and final `step.finished`. + */ +export function buildPetriStageActivity( + items: RunStreamItem[], + stage: StageProjection | undefined, + renderer: StageRenderer, +): StageActivity { + const turns: TurnType[] = []; + const pendingTools = new Map(); + const firstTs = items[0] + ? new Date(items[0].recorded_at).toISOString() + : (stage?.started_at ?? new Date(0).toISOString()); + const startTs = stage?.started_at ?? firstTs; + + if (renderer === "command") { + const started = items.some( + (item) => item.kind === "petri" && getString(getObject(getObject(item.item, "record"), "body"), "event") === "step.started", + ); + if (started || stage) { + const outcome = commandOutcomeOf(items); + const running = + stage?.state === "running" || stage?.state === "retrying"; + turns.push({ + kind: "command", + ts: startTs, + script: commandScriptOf(items) ?? "", + running, + exitCode: outcome.exitCode, + durationMs: outcome.durationMs || (stage?.timing?.wall_time_ms ?? 0), + outputBytes: stage?.output_bytes ?? 0, + }); + } + return { turns, pendingTools: [] }; + } + + const envelopes = agentEnvelopesOf(items); + // The prompt is the session's `UserInput`; the projection's `prompt` stands + // in for a stage whose session recorded none (a prompt node). + if (stage?.prompt && !envelopes.some((envelope) => envelope.variant === "UserInput")) { + turns.push({ kind: "system", ts: startTs, content: stage.prompt }); + } + let sawAssistantMessage = false; + for (const envelope of envelopes) { + const { payload } = envelope; + switch (envelope.variant) { + case "UserInput": { + const text = getString(payload, "text") ?? ""; + if (text) turns.push({ kind: "system", ts: envelope.ts, content: text }); + break; + } + case "AssistantMessage": { + sawAssistantMessage = true; + const tokens = getObject(getObject(payload, "usage"), "tokens") ?? getObject(payload, "usage") ?? {}; + turns.push({ + kind: "assistant", + ts: envelope.ts, + content: getString(payload, "text") ?? "", + inputTokens: getNumber(tokens, "input") ?? 0, + outputTokens: (getNumber(tokens, "output") ?? 0) + (getNumber(tokens, "reasoning") ?? 0), + toolCallCount: getNumber(payload, "tool_call_count") ?? null, + reasoning: readTurnReasoning(payload), + }); + break; + } + case "ToolCallStarted": { + const callId = getString(payload, "tool_call_id"); + if (!callId) break; + const args = payload.arguments; + pendingTools.set(callId, { + ts: envelope.ts, + toolName: getString(payload, "tool_name") ?? "", + input: typeof args === "string" ? args : JSON.stringify(args ?? ""), + }); + break; + } + case "ToolCallCompleted": { + const callId = getString(payload, "tool_call_id"); + if (!callId) break; + const started = pendingTools.get(callId); + pendingTools.delete(callId); + const output = payload.output ?? ""; + turns.push({ + kind: "tool", + ts: started?.ts ?? envelope.ts, + toolName: started?.toolName ?? getString(payload, "tool_name") ?? "", + input: started?.input ?? "", + result: typeof output === "string" ? output : JSON.stringify(output, null, 2), + isError: payload.is_error === true, + durationMs: durationBetween(started?.ts, envelope.ts), + }); + break; + } + case "SteeringInjected": { + const text = getString(payload, "text") ?? ""; + if (text) turns.push({ kind: "steer", ts: envelope.ts, content: text }); + break; + } + case "RoundInterrupted": + turns.push({ kind: "interrupt", ts: envelope.ts, content: "Interrupted — waiting for steering" }); + break; + default: + break; + } + } + if (!sawAssistantMessage && stage?.response) { + const tokens = stage.usage?.tokens; + turns.push({ + kind: "assistant", + ts: stage.completion?.timestamp ?? firstTs, + content: stage.response, + inputTokens: tokens?.input ?? 0, + outputTokens: tokens?.output ?? 0, + toolCallCount: null, + reasoning: null, + }); + } + + return { + turns, + pendingTools: Array.from(pendingTools, ([toolCallId, tool]) => ({ + toolCallId, + toolName: tool.toolName, + input: tool.input, + })), + }; +} + +/** A debug list item: a legacy event, or a Petri stream row. */ +type DebugListItem = EventEnvelope | DebugRow; + +function isDebugRow(item: DebugListItem): item is DebugRow { + return "item" in item && "category" in item; +} + +function debugItemSearchText(item: DebugListItem): string { + if (isDebugRow(item)) return debugRowSearchText(item); + return `${item.event ?? ""} ${JSON.stringify(item.properties ?? {})}`.toLowerCase(); +} + +/** What the details panel shows for a debug item. */ +function debugItemPayload(item: DebugListItem): EventDisplayPayload { + if (!isDebugRow(item)) return item; + return { + event: item.event, + stream_seq: item.seq, + kind: item.item.kind, + stage: item.stageLabel, + recorded_at: item.ts, + item: item.item.item, + }; +} + +/** What the Petri renderers show for one stage, derived once per stage. */ +interface PetriStageViews { + pairs: HumanInterviewPair[]; + edge: EdgeSelection | null; + overview: ParallelOverview; + reducer: ReducerTranscript | null; +} + type ToolTurn = Extract; type ToolGroupChild = { turn: ToolTurn; turnIndex: number }; type ToolGroupChildren = readonly [ @@ -2096,6 +2294,7 @@ function StageActivityBody({ contextData, runEvents, stages, + petri, }: { effectiveTab: EventsTab; renderer: StageRenderer; @@ -2107,15 +2306,18 @@ function StageActivityBody({ runId: string; selectedStage: Stage; commandTurn: CommandTurn | null; - debugEvents: EventEnvelope[]; - filteredDebugEvents: EventEnvelope[]; + debugEvents: DebugListItem[]; + filteredDebugEvents: DebugListItem[]; openDebugSeq: number | null; onDebugSeqChange: (seq: number | null) => void; contextData: ReturnType; runEvents: EventEnvelope[]; stages: Stage[]; + /** Set for a stage of a Petri run: the renderers read these, not events. */ + petri?: PetriStageViews; }) { const { turns, pendingTools } = activity; + const legacyEvents = petri ? [] : (debugEvents as EventEnvelope[]); return (
{effectiveTab === "chat" ? ( @@ -2169,23 +2371,29 @@ function StageActivityBody({ turn={commandTurn} /> ) : renderer === "human" ? ( - + ) : renderer === "conditional" ? ( ) : renderer === "parallel" ? ( ) : renderer === "fan_in" ? ( - + ) : renderer === "wait" ? ( ) : ( @@ -2227,6 +2435,7 @@ function RunStageActivityStage({ onKindsChange, onDebugCategoriesChange, onSearchChange, + petri, }: { runId: string; selectedStage: Stage; @@ -2240,25 +2449,54 @@ function RunStageActivityStage({ onKindsChange: (kinds: EventKind[]) => void; onDebugCategoriesChange: (categories: DebugCategory[]) => void; onSearchChange: (search: string) => void; + /** The run's stream and projection when it executes on Petri. */ + petri?: PetriRunData; }) { const selectedStageId = selectedStage.id; - const stageEventsQuery = useRunStageEvents(runId, selectedStageId); + const renderer: StageRenderer = selectStageRenderer(selectedStage.handler); + // A Petri run has no legacy stage events: its views read the stream and + // the projection, so the query stays idle. + const stageEventsQuery = useRunStageEvents(petri ? undefined : runId, selectedStageId); + const stageItems = useMemo( + () => (petri ? itemsForStage(petri.stream, selectedStageId) : []), + [petri, selectedStageId], + ); + const stageProjection: StageProjection | undefined = + petri?.projection.stages[selectedStageId]; const activity = useMemo( - () => buildStageActivity(stageEventsQuery.data ?? [], selectedStageId), - [stageEventsQuery.data, selectedStageId], + () => + petri + ? buildPetriStageActivity(stageItems, stageProjection, renderer) + : buildStageActivity(stageEventsQuery.data ?? [], selectedStageId), + [petri, stageItems, stageProjection, renderer, stageEventsQuery.data, selectedStageId], ); const { turns } = activity; - const renderer: StageRenderer = selectStageRenderer(selectedStage.handler); - const debugEvents = useMemo(() => { + const debugEvents = useMemo(() => { + if (petri) return debugRowsFromStream(stageItems); return (stageEventsQuery.data ?? []).filter( (event) => activityEventStageId(event) === selectedStageId, ); - }, [stageEventsQuery.data, selectedStageId]); + }, [petri, stageItems, stageEventsQuery.data, selectedStageId]); + const petriViews = useMemo( + () => + petri + ? { + pairs: parsePetriInterviewPairs(stageItems), + edge: findPetriEdgeForStage(petri.stream, selectedStageId), + overview: parallelOverviewFromProjection(stageProjection), + reducer: reducerTranscriptFromProjection(stageProjection), + } + : undefined, + [petri, stageItems, stageProjection, selectedStageId], + ); // The Context tab surfaces the workflow's deliberate per-visit outputs. It // only exists when the stage completed and actually wrote something. const contextData = useMemo( - () => extractStageContext(debugEvents), - [debugEvents], + () => + petri + ? extractPetriStageContext(stageItems) + : extractStageContext(debugEvents as EventEnvelope[]), + [petri, stageItems, debugEvents], ); const availableTabs = useMemo( () => @@ -2276,7 +2514,7 @@ function RunStageActivityStage({ // Some renderers need run-scoped events (e.g. conditional renders the // engine-level edge.selected event, which has no stage_id). Only fetch when // the active renderer actually needs it to keep this off the hot path. - const needsRunEvents = renderer === "conditional"; + const needsRunEvents = renderer === "conditional" && !petri; const runEventsQuery = useRunEventsList(needsRunEvents ? runId : undefined); const commandTurn = useMemo(() => { if (effectiveTab !== "primary" || renderer !== "command") return null; @@ -2347,34 +2585,27 @@ function RunStageActivityStage({ } return null; }, [isPrimaryAgent, displayItems, panelSelection]); - const openDebugEvent = useMemo( - () => - isDebug && openDebugSeq != null - ? (debugEvents.find((e) => e.seq === openDebugSeq) ?? null) - : null, - [isDebug, debugEvents, openDebugSeq], - ); + const openDebugEvent = useMemo(() => { + if (!isDebug || openDebugSeq == null) return null; + const item = debugEvents.find((e) => e.seq === openDebugSeq); + return item ? debugItemPayload(item) : null; + }, [isDebug, debugEvents, openDebugSeq]); const availableDebugCategories = useMemo(() => { if (!isDebug) return []; const set = new Set(); for (const event of debugEvents) { - if (event.event) set.add(debugCategory(event.event)); + if (event.event) set.add(debugRowCategory(event)); } return Array.from(set).sort(); }, [isDebug, debugEvents]); - const filteredDebugEvents = useMemo(() => { + const filteredDebugEvents = useMemo(() => { if (!isDebug) return []; const useCategoryFilter = selectedDebugCategories.length > 0; const cats = new Set(selectedDebugCategories); const needle = search.toLowerCase(); return debugEvents.filter((event) => { - const name = event.event ?? ""; - if (useCategoryFilter && !cats.has(debugCategory(name))) return false; - if (needle) { - const blob = - `${name} ${JSON.stringify(event.properties ?? {})}`.toLowerCase(); - if (!blob.includes(needle)) return false; - } + if (useCategoryFilter && !cats.has(debugRowCategory(event))) return false; + if (needle && !debugItemSearchText(event).includes(needle)) return false; return true; }); }, [isDebug, debugEvents, selectedDebugCategories, search]); @@ -2461,6 +2692,7 @@ function RunStageActivityStage({ contextData={contextData} runEvents={runEventsQuery.data ?? []} stages={stages} + petri={petriViews} />
@@ -2493,11 +2725,13 @@ function RunStageActivity({ selectedStage, stages, runStart, + petri, }: { runId: string; selectedStage: Stage; stages: Stage[]; runStart: string | undefined; + petri?: PetriRunData; }) { const [activityState, dispatchActivity] = useReducer( stageActivityReducer, @@ -2532,6 +2766,7 @@ function RunStageActivity({ onSearchChange={(nextSearch) => dispatchActivity({ type: "searchChanged", search: nextSearch }) } + petri={petri} /> ); } @@ -2552,10 +2787,20 @@ export default function RunStages() { selectedStage?.startedAt ?? runQuery.data?.timestamps.started_at ?? runQuery.data?.timestamps.created_at; - // Insights sidebar only renders for agent stages; fetch projection + context - // window only when the user is on one to keep the hot path lean. + // The projection says which engine ran the run (a Petri run's stage views + // read it and the run's stream) and feeds the insights sidebar of an + // agent stage; the context window is fetched only for one. const isAgentStage = selectedStage?.handler === "agent"; - const runStateQuery = useRunState(isAgentStage ? id : undefined); + const runStateQuery = useRunState(id); + const petri = isPetriRun(runStateQuery.data); + const streamQuery = useRunStream(petri ? id : undefined); + const petriData = useMemo( + () => + petri && runStateQuery.data && streamQuery.data + ? { stream: streamQuery.data, projection: runStateQuery.data } + : undefined, + [petri, runStateQuery.data, streamQuery.data], + ); const contextWindowQuery = useRunStageContextWindow( isAgentStage ? id : undefined, isAgentStage ? selectedStageId : undefined, @@ -2615,6 +2860,7 @@ export default function RunStages() { selectedStage={selectedStage} stages={stages} runStart={runStart} + petri={petriData} />
); diff --git a/apps/fabro-web/app/test-fixtures/petri/command.json b/apps/fabro-web/app/test-fixtures/petri/command.json new file mode 100644 index 000000000..1c3917773 --- /dev/null +++ b/apps/fabro-web/app/test-fixtures/petri/command.json @@ -0,0 +1,3418 @@ +{ + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "projection": { + "title": "Run one command", + "spec": { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "settings": { + "project": { + "name": null, + "description": null, + "metadata": {} + }, + "workflow": { + "name": null, + "description": null, + "graph": "workflow.fabro", + "metadata": {} + }, + "run": { + "goal": null, + "working_dir": null, + "metadata": {}, + "inputs": {}, + "model": { + "provider": null, + "name": null, + "fallbacks": {}, + "controls": { + "reasoning_effort": null, + "speed": null + } + }, + "git": { + "author": null + }, + "prepare": { + "steps": [], + "timeout_ms": 300000 + }, + "execution": { + "mode": "normal", + "approval": "prompt" + }, + "checkpoint": { + "exclude_globs": [], + "skip_git_hooks": false, + "commit_timeout_ms": 30000 + }, + "clone": { + "enabled": true, + "depth": 100 + }, + "run_branch": { + "enabled": true, + "push": true + }, + "environment": { + "id": "local", + "provider": "local", + "image": { + "docker": null, + "dockerfile": null + }, + "resources": { + "cpu": null, + "memory": null, + "disk": null + }, + "network": { + "mode": "allow_all", + "allow": [] + }, + "lifecycle": { + "preserve": false, + "stop_on_terminal": true, + "auto_stop": null + }, + "labels": {}, + "env": {} + }, + "notifications": {}, + "interviews": { + "provider": null, + "slack": null + }, + "agent": { + "fabro_tools": false, + "mcps": {} + }, + "hooks": [], + "scm": { + "provider": null, + "owner": null, + "repository": null, + "github": null + }, + "pull_request": null, + "artifacts": { + "include": [] + }, + "integrations": { + "github": { + "permissions": {} + } + } + } + }, + "graph": { + "name": "Command", + "nodes": { + "start": { + "id": "start", + "attrs": { + "shape": { + "String": "Mdiamond" + } + } + }, + "say": { + "id": "say", + "attrs": { + "shape": { + "String": "parallelogram" + }, + "script": { + "String": "echo hello from petri" + } + } + }, + "exit": { + "id": "exit", + "attrs": { + "shape": { + "String": "Msquare" + } + } + } + }, + "edges": [ + { + "from": "start", + "to": "say", + "attrs": {} + }, + { + "from": "say", + "to": "exit", + "attrs": {} + } + ], + "attrs": { + "goal": { + "String": "Run one command" + } + } + }, + "graph_source": "digraph Command {\n graph [goal=\"Run one command\"]\n start [shape=Mdiamond]\n exit [shape=Msquare]\n say [shape=parallelogram, script=\"echo hello from petri\"]\n start -> say -> exit\n}", + "workflow_slug": "workflow", + "workflow_version_id": "a65150b821e21c843ede6b06fe0ed2bc6af498746a3c6883f1ee4328ef6d5c55", + "target": { + "kind": "folder", + "path": "/private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpQt0Lng" + }, + "source_directory": "/private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpQt0Lng", + "provenance": { + "server": { + "version": "0.357.0-nightly.0" + }, + "subject": { + "kind": "user", + "identity": { + "issuer": "fabro:dev", + "subject": "dev" + }, + "login": "dev", + "auth_method": "dev_token" + } + }, + "spec_blob": "d2186231edf780f52421815b48f031fc4c4686c140db567df03a5837556a0f75", + "engine": { + "kind": "petri", + "graph": { + "blob": "e9b2cc84938eeb7fadc0d5e44314892be1f39c1b8e4db8fba5b81694d82aa067", + "digest": "e9b2cc84938eeb7fadc0d5e44314892be1f39c1b8e4db8fba5b81694d82aa067" + } + } + }, + "web_url": "http://localhost:3000/runs/01M2SG2E0BWG00ZBC6CHDAXKKF", + "start": { + "start_time": "2026-09-18T05:33:16.738Z" + }, + "status": { + "kind": "succeeded", + "reason": "completed" + }, + "status_updated_at": "2026-09-18T05:33:17.309Z", + "last_event_at": "2026-09-18T05:33:17.313Z", + "pending_control": null, + "checkpoints": [ + { + "seq": 23, + "checkpoint": { + "timestamp": "2026-09-18T05:33:17.013Z", + "current_node": "start", + "completed_nodes": [], + "node_retries": {}, + "context_values": {}, + "git_commit_sha": "1ccfea3fb3759c0e78bfe36cb4c2ea7ff0833690" + }, + "diff": {} + }, + { + "seq": 41, + "checkpoint": { + "timestamp": "2026-09-18T05:33:17.151Z", + "current_node": "say", + "completed_nodes": [], + "node_retries": {}, + "context_values": {}, + "git_commit_sha": "7c85082eb6f054de8419db6cf2f554a45c8e44e1" + }, + "diff": {} + }, + { + "seq": 50, + "checkpoint": { + "timestamp": "2026-09-18T05:33:17.243Z", + "current_node": "exit", + "completed_nodes": [], + "node_retries": {}, + "context_values": {}, + "git_commit_sha": "306ef7b47756945135e5000efefd4005395cbd1b" + }, + "diff": {} + } + ], + "conclusion": { + "timestamp": "2026-09-18T05:33:17.309Z", + "status": "succeeded", + "timing": { + "wall_time_ms": 571, + "inference_time_ms": 0, + "tool_time_ms": 35, + "active_time_ms": 35 + }, + "final_git_commit_sha": "306ef7b47756945135e5000efefd4005395cbd1b", + "total_retries": 0, + "diff": {} + }, + "sandbox": { + "kind": "planned", + "plan": { + "provider": "local" + } + }, + "pull_request": null, + "superseded_by": null, + "pending_interviews": {}, + "stages": { + "exit@1": { + "first_event_seq": 469, + "prompt": null, + "response": null, + "completion": { + "outcome": "succeeded", + "notes": null, + "failure_reason": null, + "timestamp": "2026-09-18T05:33:17.243Z" + }, + "provider_used": null, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "output": null, + "live_streaming": false, + "termination": "exited", + "started_at": "2026-09-18T05:33:17.151Z", + "handler": "exit", + "graph_visit": 1, + "timing": { + "wall_time_ms": 0, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, + "usage": { + "tokens": { + "input": 0, + "output": 0, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + } + }, + "state": "succeeded" + }, + "start@1": { + "first_event_seq": 60, + "prompt": null, + "response": null, + "completion": { + "outcome": "succeeded", + "notes": null, + "failure_reason": null, + "timestamp": "2026-09-18T05:33:17.013Z" + }, + "provider_used": null, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "output": "checkout: /private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpQt0Lng is not a Git repository; the workspace starts empty\n", + "output_bytes": 130, + "live_streaming": false, + "termination": "exited", + "started_at": "2026-09-18T05:33:16.742Z", + "handler": "start", + "graph_visit": 1, + "timing": { + "wall_time_ms": 10, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, + "usage": { + "tokens": { + "input": 0, + "output": 0, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + } + }, + "state": "succeeded" + }, + "say@1": { + "first_event_seq": 332, + "prompt": null, + "response": null, + "completion": { + "outcome": "succeeded", + "notes": null, + "failure_reason": null, + "timestamp": "2026-09-18T05:33:17.151Z" + }, + "provider_used": null, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "output": "hello from petri\n", + "output_bytes": 17, + "live_streaming": false, + "termination": "exited", + "started_at": "2026-09-18T05:33:17.014Z", + "handler": "command", + "graph_visit": 1, + "timing": { + "wall_time_ms": 35, + "inference_time_ms": 0, + "tool_time_ms": 35, + "active_time_ms": 35 + }, + "usage": { + "tokens": { + "input": 0, + "output": 0, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + } + }, + "state": "succeeded" + } + } + }, + "stream": [ + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 1, + "kind": "platform", + "id": "1", + "recorded_at": 1789709596683, + "item": { + "seq": 1, + "recorded_at": 1789709596683, + "record": { + "kind": "run.created", + "spec": { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "settings": { + "project": { + "name": null, + "description": null, + "metadata": {} + }, + "workflow": { + "name": null, + "description": null, + "graph": "workflow.fabro", + "metadata": {} + }, + "run": { + "goal": null, + "working_dir": null, + "metadata": {}, + "inputs": {}, + "model": { + "provider": null, + "name": null, + "fallbacks": {}, + "controls": { + "reasoning_effort": null, + "speed": null + } + }, + "git": { + "author": null + }, + "prepare": { + "steps": [], + "timeout_ms": 300000 + }, + "execution": { + "mode": "normal", + "approval": "prompt" + }, + "checkpoint": { + "exclude_globs": [], + "skip_git_hooks": false, + "commit_timeout_ms": 30000 + }, + "clone": { + "enabled": true, + "depth": 100 + }, + "run_branch": { + "enabled": true, + "push": true + }, + "environment": { + "id": "local", + "provider": "local", + "image": { + "docker": null, + "dockerfile": null + }, + "resources": { + "cpu": null, + "memory": null, + "disk": null + }, + "network": { + "mode": "allow_all", + "allow": [] + }, + "lifecycle": { + "preserve": false, + "stop_on_terminal": true, + "auto_stop": null + }, + "labels": {}, + "env": {} + }, + "notifications": {}, + "interviews": { + "provider": null, + "slack": null + }, + "agent": { + "fabro_tools": false, + "mcps": {} + }, + "hooks": [], + "scm": { + "provider": null, + "owner": null, + "repository": null, + "github": null + }, + "pull_request": null, + "artifacts": { + "include": [] + }, + "integrations": { + "github": { + "permissions": {} + } + } + } + }, + "graph": { + "name": "Command", + "nodes": { + "start": { + "id": "start", + "attrs": { + "shape": { + "String": "Mdiamond" + } + } + }, + "exit": { + "id": "exit", + "attrs": { + "shape": { + "String": "Msquare" + } + } + }, + "say": { + "id": "say", + "attrs": { + "script": { + "String": "echo hello from petri" + }, + "shape": { + "String": "parallelogram" + } + } + } + }, + "edges": [ + { + "from": "start", + "to": "say", + "attrs": {} + }, + { + "from": "say", + "to": "exit", + "attrs": {} + } + ], + "attrs": { + "goal": { + "String": "Run one command" + } + } + }, + "graph_source": "digraph Command {\n graph [goal=\"Run one command\"]\n start [shape=Mdiamond]\n exit [shape=Msquare]\n say [shape=parallelogram, script=\"echo hello from petri\"]\n start -> say -> exit\n}", + "workflow_slug": "workflow", + "workflow_version_id": "a65150b821e21c843ede6b06fe0ed2bc6af498746a3c6883f1ee4328ef6d5c55", + "target": { + "kind": "folder", + "path": "/private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpQt0Lng" + }, + "source_directory": "/private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpQt0Lng", + "provenance": { + "server": { + "version": "0.357.0-nightly.0" + }, + "subject": { + "kind": "user", + "identity": { + "issuer": "fabro:dev", + "subject": "dev" + }, + "login": "dev", + "auth_method": "dev_token" + } + }, + "spec_blob": "d2186231edf780f52421815b48f031fc4c4686c140db567df03a5837556a0f75", + "engine": { + "kind": "petri", + "graph": { + "blob": "e9b2cc84938eeb7fadc0d5e44314892be1f39c1b8e4db8fba5b81694d82aa067", + "digest": "e9b2cc84938eeb7fadc0d5e44314892be1f39c1b8e4db8fba5b81694d82aa067" + } + } + }, + "title": "Run one command", + "web_url": "http://localhost:3000/runs/01M2SG2E0BWG00ZBC6CHDAXKKF" + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 2, + "kind": "platform", + "id": "2", + "recorded_at": 1789709596727, + "item": { + "seq": 2, + "recorded_at": 1789709596727, + "record": { + "kind": "run.lifecycle", + "transition": "submitted", + "status": { + "kind": "submitted" + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 3, + "kind": "platform", + "id": "3", + "recorded_at": 1789709596731, + "item": { + "seq": 3, + "recorded_at": 1789709596731, + "record": { + "kind": "run.lifecycle", + "transition": "start_requested", + "source": "start" + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 4, + "kind": "platform", + "id": "4", + "recorded_at": 1789709596733, + "item": { + "seq": 4, + "recorded_at": 1789709596733, + "record": { + "kind": "run.lifecycle", + "transition": "runnable", + "status": { + "kind": "runnable" + }, + "source": "start_requested" + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 5, + "kind": "platform", + "id": "5", + "recorded_at": 1789709596735, + "item": { + "seq": 5, + "recorded_at": 1789709596735, + "record": { + "kind": "run.lifecycle", + "transition": "starting", + "status": { + "kind": "starting" + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 6, + "kind": "platform", + "id": "6", + "recorded_at": 1789709596736, + "item": { + "seq": 6, + "recorded_at": 1789709596736, + "record": { + "kind": "run.lifecycle", + "transition": "running", + "status": { + "kind": "running" + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 7, + "kind": "petri", + "id": "coordinator/0/0", + "recorded_at": 1789709596738, + "item": { + "id": { + "log": "coordinator", + "seq": 0, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0 + }, + "recorded_at": 1789709596738, + "record": { + "seq": 0, + "origin": "external", + "recorded_at": 1789709596738, + "body": { + "event": "run.started", + "format_version": 5, + "key": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "root": 0, + "middleware_chain": [ + "circuit-breaker" + ] + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 8, + "kind": "petri", + "id": "coordinator/1/0", + "recorded_at": 1789709596739, + "item": { + "id": { + "log": "coordinator", + "seq": 1, + "index": 0 + }, + "origin": "external", + "context": {}, + "recorded_at": 1789709596739, + "record": { + "seq": 1, + "origin": "external", + "recorded_at": 1789709596739, + "body": { + "event": "graph.registered", + "digest": "e9b2cc84938eeb7fadc0d5e44314892be1f39c1b8e4db8fba5b81694d82aa067" + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 9, + "kind": "petri", + "id": "coordinator/2/0", + "recorded_at": 1789709596740, + "item": { + "id": { + "log": "coordinator", + "seq": 2, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0 + }, + "recorded_at": 1789709596740, + "record": { + "seq": 2, + "origin": "external", + "recorded_at": 1789709596740, + "body": { + "event": "invocation.declared", + "invocation": 0, + "call": null, + "graph": "e9b2cc84938eeb7fadc0d5e44314892be1f39c1b8e4db8fba5b81694d82aa067", + "context": {}, + "secret_bindings": "none", + "sandbox": "isolated" + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 10, + "kind": "petri", + "id": "coordinator/3/0", + "recorded_at": 1789709596740, + "item": { + "id": { + "log": "coordinator", + "seq": 3, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709596740, + "record": { + "seq": 3, + "origin": "external", + "recorded_at": 1789709596740, + "body": { + "event": "execution.declared", + "execution": 0, + "invocation": 0, + "predecessor": null, + "start": { + "entry": "graph_entries", + "context": {}, + "prior_firings": {}, + "execution_index": 0, + "max_executions": 32 + }, + "middleware_state": { + "circuit-breaker": [ + 1, + { + "loop_signatures": {}, + "restart_signatures": {}, + "pending": {} + } + ] + } + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 11, + "kind": "petri", + "id": "execution 0/0/0", + "recorded_at": 1789709596741, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 0, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709596741, + "record": { + "seq": 0, + "origin": "external", + "recorded_at": 1789709596741, + "body": { + "event": "execution.started", + "entry": "graph_entries", + "context": {}, + "prior_firings": {}, + "execution_index": 0, + "max_executions": 32 + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 12, + "kind": "petri", + "id": "execution 0/1/0", + "recorded_at": 1789709596742, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 1, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709596742, + "record": { + "seq": 1, + "origin": "external", + "recorded_at": 1789709596742, + "body": { + "event": "admission.decided", + "decision_id": "execution_start", + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 13, + "kind": "petri", + "id": "execution 0/1/1", + "recorded_at": 1789709596742, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 1, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "say", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596742, + "derived": { + "event": "visit.started", + "inputs": [ + { + "edge": 2, + "generation": 0, + "payload": null, + "from": 0 + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 14, + "kind": "petri", + "id": "execution 0/1/2", + "recorded_at": 1789709596742, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 1, + "index": 2 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "say", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596742, + "derived": { + "event": "wait.state.changed", + "state": "awaiting_admission" + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 15, + "kind": "petri", + "id": "execution 0/2/0", + "recorded_at": 1789709596742, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 2, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709596742, + "record": { + "seq": 2, + "origin": "core", + "recorded_at": 1789709596742, + "body": { + "event": "token.emitted", + "edge": 2, + "generation": 0, + "payload": null, + "from": 0 + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 16, + "kind": "petri", + "id": "execution 0/3/0", + "recorded_at": 1789709596742, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 3, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "say", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596742, + "record": { + "seq": 3, + "origin": "external", + "recorded_at": 1789709596742, + "body": { + "event": "admission.decided", + "decision_id": { + "attempt_start": { + "firing": 1, + "attempt": 1 + } + }, + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 17, + "kind": "petri", + "id": "execution 0/4/0", + "recorded_at": 1789709596825, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 4, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "say", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596825, + "record": { + "seq": 4, + "origin": "external", + "recorded_at": 1789709596825, + "body": { + "event": "step.started", + "firing": 1, + "attempt": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 18, + "kind": "petri", + "id": "execution 0/4/1", + "recorded_at": 1789709596825, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 4, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "say", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596825, + "derived": { + "event": "wait.state.changed", + "state": "running" + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 19, + "kind": "petri", + "id": "execution 0/5/0", + "recorded_at": 1789709596835, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 5, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "say", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596835, + "record": { + "seq": 5, + "origin": "external", + "recorded_at": 1789709596835, + "body": { + "event": "step.progress.recorded", + "firing": 1, + "ev": { + "log": { + "stream": "stderr", + "line": "checkout: /private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpQt0Lng is not a Git repository; the workspace starts empty" + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 20, + "kind": "petri", + "id": "execution 0/6/0", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 6, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "say", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "record": { + "seq": 6, + "origin": "external", + "recorded_at": 1789709597013, + "body": { + "event": "step.progress.recorded", + "firing": 1, + "ev": { + "custom": { + "$note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 1, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "1ccfea3fb3759c0e78bfe36cb4c2ea7ff0833690", + "reused": false + } + } + } + } + } + }, + "derived": { + "parsed": { + "kind": "note", + "note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 1, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "1ccfea3fb3759c0e78bfe36cb4c2ea7ff0833690", + "reused": false + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 21, + "kind": "petri", + "id": "execution 0/7/0", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 7, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "say", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "record": { + "seq": 7, + "origin": "external", + "recorded_at": 1789709597013, + "body": { + "event": "step.finished", + "firing": 1, + "attempt": 1, + "outcome": { + "status": "success", + "output": { + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 10 + }, + "context_updates": { + "failure_class": "", + "internal.run_id": "petri" + } + } + } + }, + "derived": { + "final": true, + "exhausted": false + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 22, + "kind": "petri", + "id": "execution 0/7/1", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 7, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "say", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "derived": { + "event": "visit.completed", + "outcome": { + "status": "success", + "output": { + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 10 + }, + "context_updates": { + "failure_class": "", + "internal.run_id": "petri" + } + }, + "executed": true, + "attempts": 1 + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 23, + "kind": "platform", + "id": "7", + "recorded_at": 1789709597013, + "item": { + "seq": 7, + "recorded_at": 1789709597013, + "record": { + "kind": "checkpoint", + "execution": 0, + "firing": 1, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "1ccfea3fb3759c0e78bfe36cb4c2ea7ff0833690", + "operation": { + "execution": 0, + "decision": { + "attempt_start": { + "firing": 1, + "attempt": 1 + } + }, + "effect": "checkpoint" + } + }, + "position": { + "execution": 0, + "firing": 1 + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 24, + "kind": "petri", + "id": "execution 0/8/0", + "recorded_at": 1789709597014, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 8, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "say", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597014, + "record": { + "seq": 8, + "origin": "external", + "recorded_at": 1789709597014, + "body": { + "event": "routing.resolved", + "decision_id": { + "route": { + "firing": 1, + "attempt": 1 + } + }, + "groups": [ + { + "group": 0, + "draw": null, + "trace": [], + "decision": { + "emit": 0 + } + } + ] + } + }, + "derived": { + "groups": [ + { + "group": 0, + "target": { + "id": 2, + "name": "say", + "kind": "attractor/command", + "meta": { + "label": "say", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + } + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 25, + "kind": "petri", + "id": "execution 0/8/1", + "recorded_at": 1789709597014, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 8, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "say", + "kind": "attractor/command", + "meta": { + "label": "say", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597014, + "derived": { + "event": "visit.started", + "inputs": [ + { + "edge": 0, + "generation": 0, + "payload": { + "outcome": "succeeded", + "failure_class": "" + }, + "from": 1 + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 26, + "kind": "petri", + "id": "execution 0/8/2", + "recorded_at": 1789709597014, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 8, + "index": 2 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "say", + "kind": "attractor/command", + "meta": { + "label": "say", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597014, + "derived": { + "event": "wait.state.changed", + "state": "awaiting_admission" + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 27, + "kind": "petri", + "id": "execution 0/9/0", + "recorded_at": 1789709597014, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 9, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "say", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597014, + "record": { + "seq": 9, + "origin": "core", + "recorded_at": 1789709597014, + "body": { + "event": "route.applied", + "kind": "edge", + "firing": 1, + "group": 0, + "edge": 0 + } + }, + "derived": { + "target": { + "id": 2, + "name": "say", + "kind": "attractor/command", + "meta": { + "label": "say", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "transition": "Continue", + "back": false + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 28, + "kind": "petri", + "id": "execution 0/10/0", + "recorded_at": 1789709597014, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 10, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "say", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597014, + "record": { + "seq": 10, + "origin": "core", + "recorded_at": 1789709597014, + "body": { + "event": "token.emitted", + "edge": 0, + "generation": 0, + "payload": { + "outcome": "succeeded", + "failure_class": "" + }, + "from": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 29, + "kind": "petri", + "id": "execution 0/11/0", + "recorded_at": 1789709597014, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 11, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "say", + "kind": "attractor/command", + "meta": { + "label": "say", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597014, + "record": { + "seq": 11, + "origin": "external", + "recorded_at": 1789709597014, + "body": { + "event": "admission.decided", + "decision_id": { + "attempt_start": { + "firing": 2, + "attempt": 1 + } + }, + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 30, + "kind": "petri", + "id": "execution 0/12/0", + "recorded_at": 1789709597014, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 12, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "say", + "kind": "attractor/command", + "meta": { + "label": "say", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597014, + "record": { + "seq": 12, + "origin": "external", + "recorded_at": 1789709597014, + "body": { + "event": "step.started", + "firing": 2, + "attempt": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 31, + "kind": "petri", + "id": "execution 0/12/1", + "recorded_at": 1789709597014, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 12, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "say", + "kind": "attractor/command", + "meta": { + "label": "say", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597014, + "derived": { + "event": "wait.state.changed", + "state": "running" + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 32, + "kind": "petri", + "id": "execution 0/13/0", + "recorded_at": 1789709597021, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 13, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "say", + "kind": "attractor/command", + "meta": { + "label": "say", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597021, + "record": { + "seq": 13, + "origin": "external", + "recorded_at": 1789709597021, + "body": { + "event": "step.progress.recorded", + "firing": 2, + "ev": { + "log": { + "stream": "stdout", + "line": "hello from petri" + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 33, + "kind": "petri", + "id": "execution 0/14/0", + "recorded_at": 1789709597150, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 14, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "say", + "kind": "attractor/command", + "meta": { + "label": "say", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597150, + "record": { + "seq": 14, + "origin": "external", + "recorded_at": 1789709597150, + "body": { + "event": "step.progress.recorded", + "firing": 2, + "ev": { + "custom": { + "$note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 2, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "7c85082eb6f054de8419db6cf2f554a45c8e44e1", + "reused": false + } + } + } + } + } + }, + "derived": { + "parsed": { + "kind": "note", + "note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 2, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "7c85082eb6f054de8419db6cf2f554a45c8e44e1", + "reused": false + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 34, + "kind": "petri", + "id": "execution 0/15/0", + "recorded_at": 1789709597151, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 15, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "say", + "kind": "attractor/command", + "meta": { + "label": "say", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597151, + "record": { + "seq": 15, + "origin": "external", + "recorded_at": 1789709597151, + "body": { + "event": "step.finished", + "firing": 2, + "attempt": 1, + "outcome": { + "status": "success", + "output": { + "exit_status": 0, + "stdout": "hello from petri\n", + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 35 + }, + "context_updates": { + "command.output": "hello from petri\n", + "failure_class": "" + } + } + } + }, + "derived": { + "final": true, + "exhausted": false + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 35, + "kind": "petri", + "id": "execution 0/15/1", + "recorded_at": 1789709597151, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 15, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "say", + "kind": "attractor/command", + "meta": { + "label": "say", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597151, + "derived": { + "event": "visit.completed", + "outcome": { + "status": "success", + "output": { + "exit_status": 0, + "stdout": "hello from petri\n", + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 35 + }, + "context_updates": { + "command.output": "hello from petri\n", + "failure_class": "" + } + }, + "executed": true, + "attempts": 1 + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 36, + "kind": "petri", + "id": "execution 0/16/0", + "recorded_at": 1789709597151, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 16, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "say", + "kind": "attractor/command", + "meta": { + "label": "say", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597151, + "record": { + "seq": 16, + "origin": "external", + "recorded_at": 1789709597151, + "body": { + "event": "routing.resolved", + "decision_id": { + "route": { + "firing": 2, + "attempt": 1 + } + }, + "groups": [ + { + "group": 0, + "draw": null, + "trace": [], + "decision": { + "emit": 1 + } + } + ] + } + }, + "derived": { + "groups": [ + { + "group": 0, + "target": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + } + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 37, + "kind": "petri", + "id": "execution 0/16/1", + "recorded_at": 1789709597151, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 16, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597151, + "derived": { + "event": "visit.started", + "inputs": [ + { + "edge": 1, + "generation": 0, + "payload": { + "exit_status": 0, + "stdout": "hello from petri\n", + "outcome": "succeeded", + "failure_class": "" + }, + "from": 2 + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 38, + "kind": "petri", + "id": "execution 0/16/2", + "recorded_at": 1789709597151, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 16, + "index": 2 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597151, + "derived": { + "event": "wait.state.changed", + "state": "awaiting_admission" + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 39, + "kind": "petri", + "id": "execution 0/17/0", + "recorded_at": 1789709597151, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 17, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "say", + "kind": "attractor/command", + "meta": { + "label": "say", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597151, + "record": { + "seq": 17, + "origin": "core", + "recorded_at": 1789709597151, + "body": { + "event": "route.applied", + "kind": "edge", + "firing": 2, + "group": 0, + "edge": 1 + } + }, + "derived": { + "target": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "transition": "Continue", + "back": false + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 40, + "kind": "petri", + "id": "execution 0/18/0", + "recorded_at": 1789709597151, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 18, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "say", + "kind": "attractor/command", + "meta": { + "label": "say", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597151, + "record": { + "seq": 18, + "origin": "core", + "recorded_at": 1789709597151, + "body": { + "event": "token.emitted", + "edge": 1, + "generation": 0, + "payload": { + "exit_status": 0, + "stdout": "hello from petri\n", + "outcome": "succeeded", + "failure_class": "" + }, + "from": 2 + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 41, + "kind": "platform", + "id": "8", + "recorded_at": 1789709597151, + "item": { + "seq": 8, + "recorded_at": 1789709597151, + "record": { + "kind": "checkpoint", + "execution": 0, + "firing": 2, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "7c85082eb6f054de8419db6cf2f554a45c8e44e1", + "operation": { + "execution": 0, + "decision": { + "attempt_start": { + "firing": 2, + "attempt": 1 + } + }, + "effect": "checkpoint" + } + }, + "position": { + "execution": 0, + "firing": 2 + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 42, + "kind": "petri", + "id": "execution 0/19/0", + "recorded_at": 1789709597151, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 19, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597151, + "record": { + "seq": 19, + "origin": "external", + "recorded_at": 1789709597151, + "body": { + "event": "admission.decided", + "decision_id": { + "attempt_start": { + "firing": 3, + "attempt": 1 + } + }, + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 43, + "kind": "petri", + "id": "execution 0/20/0", + "recorded_at": 1789709597151, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 20, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597151, + "record": { + "seq": 20, + "origin": "external", + "recorded_at": 1789709597151, + "body": { + "event": "step.started", + "firing": 3, + "attempt": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 44, + "kind": "petri", + "id": "execution 0/20/1", + "recorded_at": 1789709597151, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 20, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597151, + "derived": { + "event": "wait.state.changed", + "state": "running" + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 45, + "kind": "petri", + "id": "coordinator/4/0", + "recorded_at": 1789709597243, + "item": { + "id": { + "log": "coordinator", + "seq": 4, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709597243, + "record": { + "seq": 4, + "origin": "external", + "recorded_at": 1789709597243, + "body": { + "event": "execution.finished", + "execution": 0, + "exit": { + "terminal": { + "status": "success" + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 46, + "kind": "petri", + "id": "execution 0/21/0", + "recorded_at": 1789709597243, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 21, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597243, + "record": { + "seq": 21, + "origin": "external", + "recorded_at": 1789709597243, + "body": { + "event": "step.progress.recorded", + "firing": 3, + "ev": { + "custom": { + "$note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 3, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "306ef7b47756945135e5000efefd4005395cbd1b", + "reused": false + } + } + } + } + } + }, + "derived": { + "parsed": { + "kind": "note", + "note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 3, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "306ef7b47756945135e5000efefd4005395cbd1b", + "reused": false + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 47, + "kind": "petri", + "id": "execution 0/22/0", + "recorded_at": 1789709597243, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 22, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597243, + "record": { + "seq": 22, + "origin": "external", + "recorded_at": 1789709597243, + "body": { + "event": "step.finished", + "firing": 3, + "attempt": 1, + "outcome": { + "status": "success", + "output": { + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 0 + }, + "context_updates": { + "failure_class": "" + } + } + } + }, + "derived": { + "final": true, + "exhausted": false + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 48, + "kind": "petri", + "id": "execution 0/22/1", + "recorded_at": 1789709597243, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 22, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597243, + "derived": { + "event": "visit.completed", + "outcome": { + "status": "success", + "output": { + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 0 + }, + "context_updates": { + "failure_class": "" + } + }, + "executed": true, + "attempts": 1 + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 49, + "kind": "petri", + "id": "execution 0/23/0", + "recorded_at": 1789709597243, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 23, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597243, + "record": { + "seq": 23, + "origin": "external", + "recorded_at": 1789709597243, + "body": { + "event": "routing.resolved", + "decision_id": { + "route": { + "firing": 3, + "attempt": 1 + } + }, + "groups": [] + } + }, + "derived": { + "groups": [] + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 50, + "kind": "platform", + "id": "9", + "recorded_at": 1789709597243, + "item": { + "seq": 9, + "recorded_at": 1789709597243, + "record": { + "kind": "checkpoint", + "execution": 0, + "firing": 3, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "306ef7b47756945135e5000efefd4005395cbd1b", + "operation": { + "execution": 0, + "decision": { + "attempt_start": { + "firing": 3, + "attempt": 1 + } + }, + "effect": "checkpoint" + } + }, + "position": { + "execution": 0, + "firing": 3 + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 51, + "kind": "petri", + "id": "coordinator/5/0", + "recorded_at": 1789709597243, + "item": { + "id": { + "log": "coordinator", + "seq": 5, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709597243, + "record": { + "seq": 5, + "origin": "external", + "recorded_at": 1789709597243, + "body": { + "event": "invocation.finished", + "invocation": 0, + "result": { + "status": "success", + "failure": null, + "final_execution": 0, + "output": null, + "context": { + "command.output": "hello from petri\n", + "failure_class": "", + "internal.run_id": "petri" + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 52, + "kind": "petri", + "id": "coordinator/6/0", + "recorded_at": 1789709597309, + "item": { + "id": { + "log": "coordinator", + "seq": 6, + "index": 0 + }, + "origin": "external", + "context": {}, + "recorded_at": 1789709597309, + "record": { + "seq": 6, + "origin": "external", + "recorded_at": 1789709597309, + "body": { + "event": "run.finished", + "status": "success" + } + } + } + }, + { + "run_id": "01M2SG2E0BWG00ZBC6CHDAXKKF", + "stream_seq": 53, + "kind": "platform", + "id": "10", + "recorded_at": 1789709597313, + "item": { + "seq": 10, + "recorded_at": 1789709597313, + "record": { + "kind": "run.lifecycle", + "transition": "succeeded", + "status": { + "kind": "succeeded", + "reason": "completed" + } + } + } + } + ] +} \ No newline at end of file diff --git a/apps/fabro-web/app/test-fixtures/petri/gate.json b/apps/fabro-web/app/test-fixtures/petri/gate.json new file mode 100644 index 000000000..9e13c401c --- /dev/null +++ b/apps/fabro-web/app/test-fixtures/petri/gate.json @@ -0,0 +1,4816 @@ +{ + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "projection": { + "title": "Ask before running", + "spec": { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "settings": { + "project": { + "name": null, + "description": null, + "metadata": {} + }, + "workflow": { + "name": null, + "description": null, + "graph": "workflow.fabro", + "metadata": {} + }, + "run": { + "goal": null, + "working_dir": null, + "metadata": {}, + "inputs": {}, + "model": { + "provider": null, + "name": null, + "fallbacks": {}, + "controls": { + "reasoning_effort": null, + "speed": null + } + }, + "git": { + "author": null + }, + "prepare": { + "steps": [], + "timeout_ms": 300000 + }, + "execution": { + "mode": "normal", + "approval": "prompt" + }, + "checkpoint": { + "exclude_globs": [], + "skip_git_hooks": false, + "commit_timeout_ms": 30000 + }, + "clone": { + "enabled": true, + "depth": 100 + }, + "run_branch": { + "enabled": true, + "push": true + }, + "environment": { + "id": "local", + "provider": "local", + "image": { + "docker": null, + "dockerfile": null + }, + "resources": { + "cpu": null, + "memory": null, + "disk": null + }, + "network": { + "mode": "allow_all", + "allow": [] + }, + "lifecycle": { + "preserve": false, + "stop_on_terminal": true, + "auto_stop": null + }, + "labels": {}, + "env": {} + }, + "notifications": {}, + "interviews": { + "provider": null, + "slack": null + }, + "agent": { + "fabro_tools": false, + "mcps": {} + }, + "hooks": [], + "scm": { + "provider": null, + "owner": null, + "repository": null, + "github": null + }, + "pull_request": null, + "artifacts": { + "include": [] + }, + "integrations": { + "github": { + "permissions": {} + } + } + } + }, + "graph": { + "name": "Gate", + "nodes": { + "yes": { + "id": "yes", + "attrs": { + "script": { + "String": "touch /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpdAZvaU/yes" + }, + "shape": { + "String": "parallelogram" + } + } + }, + "start": { + "id": "start", + "attrs": { + "shape": { + "String": "Mdiamond" + } + } + }, + "exit": { + "id": "exit", + "attrs": { + "shape": { + "String": "Msquare" + } + } + }, + "no": { + "id": "no", + "attrs": { + "script": { + "String": "touch /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpdAZvaU/no" + }, + "shape": { + "String": "parallelogram" + } + } + }, + "gate": { + "id": "gate", + "attrs": { + "label": { + "String": "Go?" + }, + "shape": { + "String": "hexagon" + }, + "question_type": { + "String": "yes_no" + } + } + } + }, + "edges": [ + { + "from": "start", + "to": "gate", + "attrs": {} + }, + { + "from": "gate", + "to": "yes", + "attrs": { + "label": { + "String": "[Y] Yes" + } + } + }, + { + "from": "gate", + "to": "no", + "attrs": { + "label": { + "String": "[N] No" + } + } + }, + { + "from": "yes", + "to": "exit", + "attrs": {} + }, + { + "from": "no", + "to": "exit", + "attrs": {} + } + ], + "attrs": { + "goal": { + "String": "Ask before running" + } + } + }, + "graph_source": "digraph Gate {\n graph [goal=\"Ask before running\"]\n start [shape=Mdiamond]\n exit [shape=Msquare]\n gate [shape=hexagon, label=\"Go?\", question_type=\"yes_no\"]\n yes [shape=parallelogram, script=\"touch /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpdAZvaU/yes\"]\n no [shape=parallelogram, script=\"touch /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpdAZvaU/no\"]\n start -> gate\n gate -> yes [label=\"[Y] Yes\"]\n gate -> no [label=\"[N] No\"]\n yes -> exit\n no -> exit\n}", + "workflow_slug": "workflow", + "workflow_version_id": "1d8cc56da9a35251971537e032912fbeb154d66c4ae546ed2f970c7d03fbc244", + "target": { + "kind": "folder", + "path": "/private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpdHiMg3" + }, + "source_directory": "/private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpdHiMg3", + "provenance": { + "server": { + "version": "0.357.0-nightly.0" + }, + "subject": { + "kind": "user", + "identity": { + "issuer": "fabro:dev", + "subject": "dev" + }, + "login": "dev", + "auth_method": "dev_token" + } + }, + "spec_blob": "2d6c77e9cc030208cd35d778a57411f681950485aa5b73261f5c724c9217332a", + "engine": { + "kind": "petri", + "graph": { + "blob": "2cfe649e870e1e244168106779790cce67cd186c19bd5ad7614324579e4e5281", + "digest": "2cfe649e870e1e244168106779790cce67cd186c19bd5ad7614324579e4e5281" + } + } + }, + "web_url": "http://localhost:3000/runs/01M2SG2E0BEBVXSJCT8MZE19FT", + "start": { + "start_time": "2026-09-18T05:33:16.738Z" + }, + "status": { + "kind": "succeeded", + "reason": "completed" + }, + "status_updated_at": "2026-09-18T05:33:17.412Z", + "last_event_at": "2026-09-18T05:33:17.416Z", + "pending_control": null, + "checkpoints": [ + { + "seq": 28, + "checkpoint": { + "timestamp": "2026-09-18T05:33:17.013Z", + "current_node": "start", + "completed_nodes": [], + "node_retries": {}, + "context_values": {}, + "git_commit_sha": "f8b1940177c323c419061d1d56758fdc39bfcd05" + }, + "diff": {} + }, + { + "seq": 41, + "checkpoint": { + "timestamp": "2026-09-18T05:33:17.134Z", + "current_node": "gate", + "completed_nodes": [], + "node_retries": {}, + "context_values": {}, + "git_commit_sha": "d55c6e89fe209be998b0c18db7c4590315af1841" + }, + "diff": {} + }, + { + "seq": 53, + "checkpoint": { + "timestamp": "2026-09-18T05:33:17.264Z", + "current_node": "no", + "completed_nodes": [], + "node_retries": {}, + "context_values": {}, + "git_commit_sha": "67996dc3f38bf41af0da8b24a0afebb2277554e1" + }, + "diff": {} + }, + { + "seq": 65, + "checkpoint": { + "timestamp": "2026-09-18T05:33:17.370Z", + "current_node": "exit", + "completed_nodes": [], + "node_retries": {}, + "context_values": {}, + "git_commit_sha": "49d77497eb2088a5a84c05fe03824f20621e3a3b" + }, + "diff": {} + } + ], + "conclusion": { + "timestamp": "2026-09-18T05:33:17.412Z", + "status": "succeeded", + "timing": { + "wall_time_ms": 674, + "inference_time_ms": 0, + "tool_time_ms": 35, + "active_time_ms": 35 + }, + "final_git_commit_sha": "49d77497eb2088a5a84c05fe03824f20621e3a3b", + "total_retries": 0, + "diff": {} + }, + "sandbox": { + "kind": "planned", + "plan": { + "provider": "local" + } + }, + "pull_request": null, + "superseded_by": null, + "pending_interviews": {}, + "stages": { + "no@1": { + "first_event_seq": 453, + "prompt": null, + "response": null, + "completion": { + "outcome": "succeeded", + "notes": null, + "failure_reason": null, + "timestamp": "2026-09-18T05:33:17.264Z" + }, + "provider_used": null, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "output": null, + "live_streaming": false, + "termination": "exited", + "started_at": "2026-09-18T05:33:17.135Z", + "handler": "command", + "graph_visit": 1, + "timing": { + "wall_time_ms": 35, + "inference_time_ms": 0, + "tool_time_ms": 35, + "active_time_ms": 35 + }, + "usage": { + "tokens": { + "input": 0, + "output": 0, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + } + }, + "state": "succeeded" + }, + "exit@1": { + "first_event_seq": 583, + "prompt": null, + "response": null, + "completion": { + "outcome": "succeeded", + "notes": null, + "failure_reason": null, + "timestamp": "2026-09-18T05:33:17.370Z" + }, + "provider_used": null, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "output": null, + "live_streaming": false, + "termination": "exited", + "started_at": "2026-09-18T05:33:17.265Z", + "handler": "exit", + "graph_visit": 1, + "timing": { + "wall_time_ms": 0, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, + "usage": { + "tokens": { + "input": 0, + "output": 0, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + } + }, + "state": "succeeded" + }, + "gate@1": { + "first_event_seq": 331, + "prompt": null, + "response": null, + "completion": { + "outcome": "succeeded", + "notes": null, + "failure_reason": null, + "timestamp": "2026-09-18T05:33:17.134Z" + }, + "provider_used": null, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "output": "waiting for an answer: Go?\n", + "output_bytes": 27, + "live_streaming": false, + "termination": "exited", + "started_at": "2026-09-18T05:33:17.013Z", + "handler": "human", + "graph_visit": 1, + "timing": { + "wall_time_ms": 17, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, + "usage": { + "tokens": { + "input": 0, + "output": 0, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + } + }, + "state": "succeeded" + }, + "start@1": { + "first_event_seq": 60, + "prompt": null, + "response": null, + "completion": { + "outcome": "succeeded", + "notes": null, + "failure_reason": null, + "timestamp": "2026-09-18T05:33:17.013Z" + }, + "provider_used": null, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "output": "checkout: /private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpdHiMg3 is not a Git repository; the workspace starts empty\n", + "output_bytes": 130, + "live_streaming": false, + "termination": "exited", + "started_at": "2026-09-18T05:33:16.742Z", + "handler": "start", + "graph_visit": 1, + "timing": { + "wall_time_ms": 10, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, + "usage": { + "tokens": { + "input": 0, + "output": 0, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + } + }, + "state": "succeeded" + } + } + }, + "stream": [ + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 1, + "kind": "platform", + "id": "1", + "recorded_at": 1789709596683, + "item": { + "seq": 1, + "recorded_at": 1789709596683, + "record": { + "kind": "run.created", + "spec": { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "settings": { + "project": { + "name": null, + "description": null, + "metadata": {} + }, + "workflow": { + "name": null, + "description": null, + "graph": "workflow.fabro", + "metadata": {} + }, + "run": { + "goal": null, + "working_dir": null, + "metadata": {}, + "inputs": {}, + "model": { + "provider": null, + "name": null, + "fallbacks": {}, + "controls": { + "reasoning_effort": null, + "speed": null + } + }, + "git": { + "author": null + }, + "prepare": { + "steps": [], + "timeout_ms": 300000 + }, + "execution": { + "mode": "normal", + "approval": "prompt" + }, + "checkpoint": { + "exclude_globs": [], + "skip_git_hooks": false, + "commit_timeout_ms": 30000 + }, + "clone": { + "enabled": true, + "depth": 100 + }, + "run_branch": { + "enabled": true, + "push": true + }, + "environment": { + "id": "local", + "provider": "local", + "image": { + "docker": null, + "dockerfile": null + }, + "resources": { + "cpu": null, + "memory": null, + "disk": null + }, + "network": { + "mode": "allow_all", + "allow": [] + }, + "lifecycle": { + "preserve": false, + "stop_on_terminal": true, + "auto_stop": null + }, + "labels": {}, + "env": {} + }, + "notifications": {}, + "interviews": { + "provider": null, + "slack": null + }, + "agent": { + "fabro_tools": false, + "mcps": {} + }, + "hooks": [], + "scm": { + "provider": null, + "owner": null, + "repository": null, + "github": null + }, + "pull_request": null, + "artifacts": { + "include": [] + }, + "integrations": { + "github": { + "permissions": {} + } + } + } + }, + "graph": { + "name": "Gate", + "nodes": { + "exit": { + "id": "exit", + "attrs": { + "shape": { + "String": "Msquare" + } + } + }, + "gate": { + "id": "gate", + "attrs": { + "label": { + "String": "Go?" + }, + "question_type": { + "String": "yes_no" + }, + "shape": { + "String": "hexagon" + } + } + }, + "no": { + "id": "no", + "attrs": { + "shape": { + "String": "parallelogram" + }, + "script": { + "String": "touch /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpdAZvaU/no" + } + } + }, + "start": { + "id": "start", + "attrs": { + "shape": { + "String": "Mdiamond" + } + } + }, + "yes": { + "id": "yes", + "attrs": { + "shape": { + "String": "parallelogram" + }, + "script": { + "String": "touch /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpdAZvaU/yes" + } + } + } + }, + "edges": [ + { + "from": "start", + "to": "gate", + "attrs": {} + }, + { + "from": "gate", + "to": "yes", + "attrs": { + "label": { + "String": "[Y] Yes" + } + } + }, + { + "from": "gate", + "to": "no", + "attrs": { + "label": { + "String": "[N] No" + } + } + }, + { + "from": "yes", + "to": "exit", + "attrs": {} + }, + { + "from": "no", + "to": "exit", + "attrs": {} + } + ], + "attrs": { + "goal": { + "String": "Ask before running" + } + } + }, + "graph_source": "digraph Gate {\n graph [goal=\"Ask before running\"]\n start [shape=Mdiamond]\n exit [shape=Msquare]\n gate [shape=hexagon, label=\"Go?\", question_type=\"yes_no\"]\n yes [shape=parallelogram, script=\"touch /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpdAZvaU/yes\"]\n no [shape=parallelogram, script=\"touch /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpdAZvaU/no\"]\n start -> gate\n gate -> yes [label=\"[Y] Yes\"]\n gate -> no [label=\"[N] No\"]\n yes -> exit\n no -> exit\n}", + "workflow_slug": "workflow", + "workflow_version_id": "1d8cc56da9a35251971537e032912fbeb154d66c4ae546ed2f970c7d03fbc244", + "target": { + "kind": "folder", + "path": "/private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpdHiMg3" + }, + "source_directory": "/private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpdHiMg3", + "provenance": { + "server": { + "version": "0.357.0-nightly.0" + }, + "subject": { + "kind": "user", + "identity": { + "issuer": "fabro:dev", + "subject": "dev" + }, + "login": "dev", + "auth_method": "dev_token" + } + }, + "spec_blob": "2d6c77e9cc030208cd35d778a57411f681950485aa5b73261f5c724c9217332a", + "engine": { + "kind": "petri", + "graph": { + "blob": "2cfe649e870e1e244168106779790cce67cd186c19bd5ad7614324579e4e5281", + "digest": "2cfe649e870e1e244168106779790cce67cd186c19bd5ad7614324579e4e5281" + } + } + }, + "title": "Ask before running", + "web_url": "http://localhost:3000/runs/01M2SG2E0BEBVXSJCT8MZE19FT" + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 2, + "kind": "platform", + "id": "2", + "recorded_at": 1789709596727, + "item": { + "seq": 2, + "recorded_at": 1789709596727, + "record": { + "kind": "run.lifecycle", + "transition": "submitted", + "status": { + "kind": "submitted" + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 3, + "kind": "platform", + "id": "3", + "recorded_at": 1789709596732, + "item": { + "seq": 3, + "recorded_at": 1789709596732, + "record": { + "kind": "run.lifecycle", + "transition": "start_requested", + "source": "start" + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 4, + "kind": "platform", + "id": "4", + "recorded_at": 1789709596733, + "item": { + "seq": 4, + "recorded_at": 1789709596733, + "record": { + "kind": "run.lifecycle", + "transition": "runnable", + "status": { + "kind": "runnable" + }, + "source": "start_requested" + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 5, + "kind": "platform", + "id": "5", + "recorded_at": 1789709596735, + "item": { + "seq": 5, + "recorded_at": 1789709596735, + "record": { + "kind": "run.lifecycle", + "transition": "starting", + "status": { + "kind": "starting" + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 6, + "kind": "platform", + "id": "6", + "recorded_at": 1789709596736, + "item": { + "seq": 6, + "recorded_at": 1789709596736, + "record": { + "kind": "run.lifecycle", + "transition": "running", + "status": { + "kind": "running" + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 7, + "kind": "petri", + "id": "coordinator/0/0", + "recorded_at": 1789709596738, + "item": { + "id": { + "log": "coordinator", + "seq": 0, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0 + }, + "recorded_at": 1789709596738, + "record": { + "seq": 0, + "origin": "external", + "recorded_at": 1789709596738, + "body": { + "event": "run.started", + "format_version": 5, + "key": "01M2SG2E0BEBVXSJCT8MZE19FT", + "root": 0, + "middleware_chain": [ + "circuit-breaker" + ] + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 8, + "kind": "petri", + "id": "coordinator/1/0", + "recorded_at": 1789709596739, + "item": { + "id": { + "log": "coordinator", + "seq": 1, + "index": 0 + }, + "origin": "external", + "context": {}, + "recorded_at": 1789709596739, + "record": { + "seq": 1, + "origin": "external", + "recorded_at": 1789709596739, + "body": { + "event": "graph.registered", + "digest": "2cfe649e870e1e244168106779790cce67cd186c19bd5ad7614324579e4e5281" + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 9, + "kind": "petri", + "id": "coordinator/2/0", + "recorded_at": 1789709596740, + "item": { + "id": { + "log": "coordinator", + "seq": 2, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0 + }, + "recorded_at": 1789709596740, + "record": { + "seq": 2, + "origin": "external", + "recorded_at": 1789709596740, + "body": { + "event": "invocation.declared", + "invocation": 0, + "call": null, + "graph": "2cfe649e870e1e244168106779790cce67cd186c19bd5ad7614324579e4e5281", + "context": {}, + "secret_bindings": "none", + "sandbox": "isolated" + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 10, + "kind": "petri", + "id": "coordinator/3/0", + "recorded_at": 1789709596740, + "item": { + "id": { + "log": "coordinator", + "seq": 3, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709596740, + "record": { + "seq": 3, + "origin": "external", + "recorded_at": 1789709596740, + "body": { + "event": "execution.declared", + "execution": 0, + "invocation": 0, + "predecessor": null, + "start": { + "entry": "graph_entries", + "context": {}, + "prior_firings": {}, + "execution_index": 0, + "max_executions": 32 + }, + "middleware_state": { + "circuit-breaker": [ + 1, + { + "loop_signatures": {}, + "restart_signatures": {}, + "pending": {} + } + ] + } + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 11, + "kind": "petri", + "id": "execution 0/0/0", + "recorded_at": 1789709596741, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 0, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709596741, + "record": { + "seq": 0, + "origin": "external", + "recorded_at": 1789709596741, + "body": { + "event": "execution.started", + "entry": "graph_entries", + "context": {}, + "prior_firings": {}, + "execution_index": 0, + "max_executions": 32 + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 12, + "kind": "petri", + "id": "execution 0/1/0", + "recorded_at": 1789709596742, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 1, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709596742, + "record": { + "seq": 1, + "origin": "external", + "recorded_at": 1789709596742, + "body": { + "event": "admission.decided", + "decision_id": "execution_start", + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 13, + "kind": "petri", + "id": "execution 0/1/1", + "recorded_at": 1789709596742, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 1, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "gate", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596742, + "derived": { + "event": "visit.started", + "inputs": [ + { + "edge": 5, + "generation": 0, + "payload": null, + "from": 0 + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 14, + "kind": "petri", + "id": "execution 0/1/2", + "recorded_at": 1789709596742, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 1, + "index": 2 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "gate", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596742, + "derived": { + "event": "wait.state.changed", + "state": "awaiting_admission" + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 15, + "kind": "petri", + "id": "execution 0/2/0", + "recorded_at": 1789709596742, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 2, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709596742, + "record": { + "seq": 2, + "origin": "core", + "recorded_at": 1789709596742, + "body": { + "event": "token.emitted", + "edge": 5, + "generation": 0, + "payload": null, + "from": 0 + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 16, + "kind": "petri", + "id": "execution 0/3/0", + "recorded_at": 1789709596742, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 3, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "gate", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596742, + "record": { + "seq": 3, + "origin": "external", + "recorded_at": 1789709596742, + "body": { + "event": "admission.decided", + "decision_id": { + "attempt_start": { + "firing": 1, + "attempt": 1 + } + }, + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 17, + "kind": "petri", + "id": "execution 0/4/0", + "recorded_at": 1789709596825, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 4, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "gate", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596825, + "record": { + "seq": 4, + "origin": "external", + "recorded_at": 1789709596825, + "body": { + "event": "step.started", + "firing": 1, + "attempt": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 18, + "kind": "petri", + "id": "execution 0/4/1", + "recorded_at": 1789709596825, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 4, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "gate", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596825, + "derived": { + "event": "wait.state.changed", + "state": "running" + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 19, + "kind": "petri", + "id": "execution 0/5/0", + "recorded_at": 1789709596836, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 5, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "gate", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596836, + "record": { + "seq": 5, + "origin": "external", + "recorded_at": 1789709596836, + "body": { + "event": "step.progress.recorded", + "firing": 1, + "ev": { + "log": { + "stream": "stderr", + "line": "checkout: /private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpdHiMg3 is not a Git repository; the workspace starts empty" + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 20, + "kind": "petri", + "id": "execution 0/6/0", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 6, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "gate", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "record": { + "seq": 6, + "origin": "external", + "recorded_at": 1789709597013, + "body": { + "event": "step.progress.recorded", + "firing": 1, + "ev": { + "custom": { + "$note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 1, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "f8b1940177c323c419061d1d56758fdc39bfcd05", + "reused": false + } + } + } + } + } + }, + "derived": { + "parsed": { + "kind": "note", + "note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 1, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "f8b1940177c323c419061d1d56758fdc39bfcd05", + "reused": false + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 21, + "kind": "petri", + "id": "execution 0/7/0", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 7, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "gate", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "record": { + "seq": 7, + "origin": "external", + "recorded_at": 1789709597013, + "body": { + "event": "step.finished", + "firing": 1, + "attempt": 1, + "outcome": { + "status": "success", + "output": { + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 10 + }, + "context_updates": { + "failure_class": "", + "internal.run_id": "petri" + } + } + } + }, + "derived": { + "final": true, + "exhausted": false + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 22, + "kind": "petri", + "id": "execution 0/7/1", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 7, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "gate", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "derived": { + "event": "visit.completed", + "outcome": { + "status": "success", + "output": { + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 10 + }, + "context_updates": { + "failure_class": "", + "internal.run_id": "petri" + } + }, + "executed": true, + "attempts": 1 + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 23, + "kind": "petri", + "id": "execution 0/8/0", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 8, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "gate", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "record": { + "seq": 8, + "origin": "external", + "recorded_at": 1789709597013, + "body": { + "event": "routing.resolved", + "decision_id": { + "route": { + "firing": 1, + "attempt": 1 + } + }, + "groups": [ + { + "group": 0, + "draw": null, + "trace": [], + "decision": { + "emit": 0 + } + } + ] + } + }, + "derived": { + "groups": [ + { + "group": 0, + "target": { + "id": 2, + "name": "gate", + "kind": "attractor/human", + "meta": { + "label": "Go?", + "shape": "hexagon", + "kind": "human", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "yes", + "label": "[Y] Yes" + }, + "2": { + "to": "no", + "label": "[N] No" + } + } + } + } + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 24, + "kind": "petri", + "id": "execution 0/8/1", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 8, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "gate", + "kind": "attractor/human", + "meta": { + "label": "Go?", + "shape": "hexagon", + "kind": "human", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "yes", + "label": "[Y] Yes" + }, + "2": { + "to": "no", + "label": "[N] No" + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "derived": { + "event": "visit.started", + "inputs": [ + { + "edge": 0, + "generation": 0, + "payload": { + "outcome": "succeeded", + "failure_class": "" + }, + "from": 1 + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 25, + "kind": "petri", + "id": "execution 0/8/2", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 8, + "index": 2 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "gate", + "kind": "attractor/human", + "meta": { + "label": "Go?", + "shape": "hexagon", + "kind": "human", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "yes", + "label": "[Y] Yes" + }, + "2": { + "to": "no", + "label": "[N] No" + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "derived": { + "event": "wait.state.changed", + "state": "awaiting_admission" + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 26, + "kind": "petri", + "id": "execution 0/9/0", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 9, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "gate", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "record": { + "seq": 9, + "origin": "core", + "recorded_at": 1789709597013, + "body": { + "event": "route.applied", + "kind": "edge", + "firing": 1, + "group": 0, + "edge": 0 + } + }, + "derived": { + "target": { + "id": 2, + "name": "gate", + "kind": "attractor/human", + "meta": { + "label": "Go?", + "shape": "hexagon", + "kind": "human", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "yes", + "label": "[Y] Yes" + }, + "2": { + "to": "no", + "label": "[N] No" + } + } + } + }, + "transition": "Continue", + "back": false + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 27, + "kind": "petri", + "id": "execution 0/10/0", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 10, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "gate", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "record": { + "seq": 10, + "origin": "core", + "recorded_at": 1789709597013, + "body": { + "event": "token.emitted", + "edge": 0, + "generation": 0, + "payload": { + "outcome": "succeeded", + "failure_class": "" + }, + "from": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 28, + "kind": "platform", + "id": "7", + "recorded_at": 1789709597013, + "item": { + "seq": 7, + "recorded_at": 1789709597013, + "record": { + "kind": "checkpoint", + "execution": 0, + "firing": 1, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "f8b1940177c323c419061d1d56758fdc39bfcd05", + "operation": { + "execution": 0, + "decision": { + "attempt_start": { + "firing": 1, + "attempt": 1 + } + }, + "effect": "checkpoint" + } + }, + "position": { + "execution": 0, + "firing": 1 + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 29, + "kind": "petri", + "id": "execution 0/11/0", + "recorded_at": 1789709597014, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 11, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "gate", + "kind": "attractor/human", + "meta": { + "label": "Go?", + "shape": "hexagon", + "kind": "human", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "yes", + "label": "[Y] Yes" + }, + "2": { + "to": "no", + "label": "[N] No" + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597014, + "record": { + "seq": 11, + "origin": "external", + "recorded_at": 1789709597014, + "body": { + "event": "admission.decided", + "decision_id": { + "attempt_start": { + "firing": 2, + "attempt": 1 + } + }, + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 30, + "kind": "petri", + "id": "execution 0/12/0", + "recorded_at": 1789709597014, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 12, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "gate", + "kind": "attractor/human", + "meta": { + "label": "Go?", + "shape": "hexagon", + "kind": "human", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "yes", + "label": "[Y] Yes" + }, + "2": { + "to": "no", + "label": "[N] No" + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597014, + "record": { + "seq": 12, + "origin": "external", + "recorded_at": 1789709597014, + "body": { + "event": "step.started", + "firing": 2, + "attempt": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 31, + "kind": "petri", + "id": "execution 0/12/1", + "recorded_at": 1789709597014, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 12, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "gate", + "kind": "attractor/human", + "meta": { + "label": "Go?", + "shape": "hexagon", + "kind": "human", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "yes", + "label": "[Y] Yes" + }, + "2": { + "to": "no", + "label": "[N] No" + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597014, + "derived": { + "event": "wait.state.changed", + "state": "running" + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 32, + "kind": "petri", + "id": "execution 0/13/0", + "recorded_at": 1789709597014, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 13, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "gate", + "kind": "attractor/human", + "meta": { + "label": "Go?", + "shape": "hexagon", + "kind": "human", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "yes", + "label": "[Y] Yes" + }, + "2": { + "to": "no", + "label": "[N] No" + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597014, + "record": { + "seq": 13, + "origin": "external", + "recorded_at": 1789709597014, + "body": { + "event": "step.progress.recorded", + "firing": 2, + "ev": { + "custom": { + "$question": { + "id": "gate#2", + "text": "Go?", + "options": [ + { + "key": "Y", + "label": "[Y] Yes" + }, + { + "key": "N", + "label": "[N] No" + } + ], + "default": "Y", + "freeform": false, + "sensitive": false, + "kind": "yes_no" + } + } + } + } + }, + "derived": { + "parsed": { + "kind": "question", + "question": { + "id": "gate#2", + "text": "Go?", + "options": [ + { + "key": "Y", + "label": "[Y] Yes" + }, + { + "key": "N", + "label": "[N] No" + } + ], + "default": "Y", + "freeform": false, + "sensitive": false, + "kind": "yes_no" + } + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 33, + "kind": "petri", + "id": "execution 0/13/1", + "recorded_at": 1789709597014, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 13, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "gate", + "kind": "attractor/human", + "meta": { + "label": "Go?", + "shape": "hexagon", + "kind": "human", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "yes", + "label": "[Y] Yes" + }, + "2": { + "to": "no", + "label": "[N] No" + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597014, + "derived": { + "event": "wait.state.changed", + "state": "awaiting_answer" + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 34, + "kind": "petri", + "id": "execution 0/14/0", + "recorded_at": 1789709597014, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 14, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "gate", + "kind": "attractor/human", + "meta": { + "label": "Go?", + "shape": "hexagon", + "kind": "human", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "yes", + "label": "[Y] Yes" + }, + "2": { + "to": "no", + "label": "[N] No" + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597014, + "record": { + "seq": 14, + "origin": "external", + "recorded_at": 1789709597014, + "body": { + "event": "step.progress.recorded", + "firing": 2, + "ev": { + "log": { + "stream": "stdout", + "line": "waiting for an answer: Go?" + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 35, + "kind": "petri", + "id": "execution 0/15/0", + "recorded_at": 1789709597031, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 15, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "gate", + "kind": "attractor/human", + "meta": { + "label": "Go?", + "shape": "hexagon", + "kind": "human", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "yes", + "label": "[Y] Yes" + }, + "2": { + "to": "no", + "label": "[N] No" + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597031, + "record": { + "seq": 15, + "origin": "external", + "recorded_at": 1789709597031, + "body": { + "event": "control.requested", + "firing": 2, + "ctl": { + "deliver": { + "$answer": { + "question": "gate#2", + "choice": "N" + } + } + } + } + }, + "derived": { + "deliverable": true, + "answer": { + "question": "gate#2", + "choice": "N" + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 36, + "kind": "petri", + "id": "execution 0/15/1", + "recorded_at": 1789709597031, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 15, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "gate", + "kind": "attractor/human", + "meta": { + "label": "Go?", + "shape": "hexagon", + "kind": "human", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "yes", + "label": "[Y] Yes" + }, + "2": { + "to": "no", + "label": "[N] No" + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597031, + "derived": { + "event": "wait.state.changed", + "state": "running" + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 37, + "kind": "platform", + "id": "8", + "recorded_at": 1789709597031, + "item": { + "seq": 8, + "recorded_at": 1789709597031, + "record": { + "kind": "interview.answered", + "question": "gate#2", + "principal": { + "kind": "user", + "identity": { + "issuer": "fabro:dev", + "subject": "dev" + }, + "login": "dev", + "auth_method": "dev_token" + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 38, + "kind": "petri", + "id": "execution 0/16/0", + "recorded_at": 1789709597134, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 16, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "gate", + "kind": "attractor/human", + "meta": { + "label": "Go?", + "shape": "hexagon", + "kind": "human", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "yes", + "label": "[Y] Yes" + }, + "2": { + "to": "no", + "label": "[N] No" + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597134, + "record": { + "seq": 16, + "origin": "external", + "recorded_at": 1789709597134, + "body": { + "event": "step.progress.recorded", + "firing": 2, + "ev": { + "custom": { + "$note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 2, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "d55c6e89fe209be998b0c18db7c4590315af1841", + "reused": false + } + } + } + } + } + }, + "derived": { + "parsed": { + "kind": "note", + "note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 2, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "d55c6e89fe209be998b0c18db7c4590315af1841", + "reused": false + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 39, + "kind": "petri", + "id": "execution 0/17/0", + "recorded_at": 1789709597134, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 17, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "gate", + "kind": "attractor/human", + "meta": { + "label": "Go?", + "shape": "hexagon", + "kind": "human", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "yes", + "label": "[Y] Yes" + }, + "2": { + "to": "no", + "label": "[N] No" + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597134, + "record": { + "seq": 17, + "origin": "external", + "recorded_at": 1789709597134, + "body": { + "event": "step.finished", + "firing": 2, + "attempt": 1, + "outcome": { + "status": "success", + "output": { + "preferred_label": "No", + "suggested_next_ids": [ + "no" + ], + "choice": "N", + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 17 + }, + "context_updates": { + "failure_class": "", + "human.gate.gate.answer": "no", + "human.gate.gate.label": "[N] No", + "human.gate.gate.question": "Go?", + "human.gate.label": "[N] No", + "human.gate.selected": "N" + } + } + } + }, + "derived": { + "final": true, + "exhausted": false + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 40, + "kind": "petri", + "id": "execution 0/17/1", + "recorded_at": 1789709597134, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 17, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "gate", + "kind": "attractor/human", + "meta": { + "label": "Go?", + "shape": "hexagon", + "kind": "human", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "yes", + "label": "[Y] Yes" + }, + "2": { + "to": "no", + "label": "[N] No" + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597134, + "derived": { + "event": "visit.completed", + "outcome": { + "status": "success", + "output": { + "preferred_label": "No", + "suggested_next_ids": [ + "no" + ], + "choice": "N", + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 17 + }, + "context_updates": { + "failure_class": "", + "human.gate.gate.answer": "no", + "human.gate.gate.label": "[N] No", + "human.gate.gate.question": "Go?", + "human.gate.label": "[N] No", + "human.gate.selected": "N" + } + }, + "executed": true, + "attempts": 1 + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 41, + "kind": "platform", + "id": "9", + "recorded_at": 1789709597134, + "item": { + "seq": 9, + "recorded_at": 1789709597134, + "record": { + "kind": "checkpoint", + "execution": 0, + "firing": 2, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "d55c6e89fe209be998b0c18db7c4590315af1841", + "operation": { + "execution": 0, + "decision": { + "attempt_start": { + "firing": 2, + "attempt": 1 + } + }, + "effect": "checkpoint" + } + }, + "position": { + "execution": 0, + "firing": 2 + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 42, + "kind": "petri", + "id": "execution 0/18/0", + "recorded_at": 1789709597135, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 18, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "gate", + "kind": "attractor/human", + "meta": { + "label": "Go?", + "shape": "hexagon", + "kind": "human", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "yes", + "label": "[Y] Yes" + }, + "2": { + "to": "no", + "label": "[N] No" + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597135, + "record": { + "seq": 18, + "origin": "external", + "recorded_at": 1789709597135, + "body": { + "event": "routing.resolved", + "decision_id": { + "route": { + "firing": 2, + "attempt": 1 + } + }, + "groups": [ + { + "group": 0, + "draw": null, + "trace": [], + "decision": { + "emit": 2 + } + } + ] + } + }, + "derived": { + "groups": [ + { + "group": 0, + "target": { + "id": 4, + "name": "no", + "kind": "attractor/command", + "meta": { + "label": "no", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "4": { + "to": "exit", + "label": null + } + } + } + } + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 43, + "kind": "petri", + "id": "execution 0/18/1", + "recorded_at": 1789709597135, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 18, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "no", + "kind": "attractor/command", + "meta": { + "label": "no", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "4": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597135, + "derived": { + "event": "visit.started", + "inputs": [ + { + "edge": 2, + "generation": 0, + "payload": { + "preferred_label": "No", + "suggested_next_ids": [ + "no" + ], + "choice": "N", + "outcome": "succeeded", + "failure_class": "" + }, + "from": 2 + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 44, + "kind": "petri", + "id": "execution 0/18/2", + "recorded_at": 1789709597135, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 18, + "index": 2 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "no", + "kind": "attractor/command", + "meta": { + "label": "no", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "4": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597135, + "derived": { + "event": "wait.state.changed", + "state": "awaiting_admission" + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 45, + "kind": "petri", + "id": "execution 0/19/0", + "recorded_at": 1789709597135, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 19, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "gate", + "kind": "attractor/human", + "meta": { + "label": "Go?", + "shape": "hexagon", + "kind": "human", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "yes", + "label": "[Y] Yes" + }, + "2": { + "to": "no", + "label": "[N] No" + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597135, + "record": { + "seq": 19, + "origin": "core", + "recorded_at": 1789709597135, + "body": { + "event": "route.applied", + "kind": "edge", + "firing": 2, + "group": 0, + "edge": 2 + } + }, + "derived": { + "target": { + "id": 4, + "name": "no", + "kind": "attractor/command", + "meta": { + "label": "no", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "4": { + "to": "exit", + "label": null + } + } + } + }, + "transition": "Continue", + "back": false + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 46, + "kind": "petri", + "id": "execution 0/20/0", + "recorded_at": 1789709597135, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 20, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "gate", + "kind": "attractor/human", + "meta": { + "label": "Go?", + "shape": "hexagon", + "kind": "human", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "edges": { + "1": { + "to": "yes", + "label": "[Y] Yes" + }, + "2": { + "to": "no", + "label": "[N] No" + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597135, + "record": { + "seq": 20, + "origin": "core", + "recorded_at": 1789709597135, + "body": { + "event": "token.emitted", + "edge": 2, + "generation": 0, + "payload": { + "preferred_label": "No", + "suggested_next_ids": [ + "no" + ], + "choice": "N", + "outcome": "succeeded", + "failure_class": "" + }, + "from": 2 + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 47, + "kind": "petri", + "id": "execution 0/21/0", + "recorded_at": 1789709597135, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 21, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "no", + "kind": "attractor/command", + "meta": { + "label": "no", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "4": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597135, + "record": { + "seq": 21, + "origin": "external", + "recorded_at": 1789709597135, + "body": { + "event": "admission.decided", + "decision_id": { + "attempt_start": { + "firing": 3, + "attempt": 1 + } + }, + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 48, + "kind": "petri", + "id": "execution 0/22/0", + "recorded_at": 1789709597135, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 22, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "no", + "kind": "attractor/command", + "meta": { + "label": "no", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "4": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597135, + "record": { + "seq": 22, + "origin": "external", + "recorded_at": 1789709597135, + "body": { + "event": "step.started", + "firing": 3, + "attempt": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 49, + "kind": "petri", + "id": "execution 0/22/1", + "recorded_at": 1789709597135, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 22, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "no", + "kind": "attractor/command", + "meta": { + "label": "no", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "4": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597135, + "derived": { + "event": "wait.state.changed", + "state": "running" + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 50, + "kind": "petri", + "id": "execution 0/23/0", + "recorded_at": 1789709597264, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 23, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "no", + "kind": "attractor/command", + "meta": { + "label": "no", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "4": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597264, + "record": { + "seq": 23, + "origin": "external", + "recorded_at": 1789709597264, + "body": { + "event": "step.progress.recorded", + "firing": 3, + "ev": { + "custom": { + "$note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 3, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "67996dc3f38bf41af0da8b24a0afebb2277554e1", + "reused": false + } + } + } + } + } + }, + "derived": { + "parsed": { + "kind": "note", + "note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 3, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "67996dc3f38bf41af0da8b24a0afebb2277554e1", + "reused": false + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 51, + "kind": "petri", + "id": "execution 0/24/0", + "recorded_at": 1789709597264, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 24, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "no", + "kind": "attractor/command", + "meta": { + "label": "no", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "4": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597264, + "record": { + "seq": 24, + "origin": "external", + "recorded_at": 1789709597264, + "body": { + "event": "step.finished", + "firing": 3, + "attempt": 1, + "outcome": { + "status": "success", + "output": { + "exit_status": 0, + "stdout": "", + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 35 + }, + "context_updates": { + "command.output": "", + "failure_class": "" + } + } + } + }, + "derived": { + "final": true, + "exhausted": false + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 52, + "kind": "petri", + "id": "execution 0/24/1", + "recorded_at": 1789709597264, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 24, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "no", + "kind": "attractor/command", + "meta": { + "label": "no", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "4": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597264, + "derived": { + "event": "visit.completed", + "outcome": { + "status": "success", + "output": { + "exit_status": 0, + "stdout": "", + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 35 + }, + "context_updates": { + "command.output": "", + "failure_class": "" + } + }, + "executed": true, + "attempts": 1 + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 53, + "kind": "platform", + "id": "10", + "recorded_at": 1789709597264, + "item": { + "seq": 10, + "recorded_at": 1789709597264, + "record": { + "kind": "checkpoint", + "execution": 0, + "firing": 3, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "67996dc3f38bf41af0da8b24a0afebb2277554e1", + "operation": { + "execution": 0, + "decision": { + "attempt_start": { + "firing": 3, + "attempt": 1 + } + }, + "effect": "checkpoint" + } + }, + "position": { + "execution": 0, + "firing": 3 + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 54, + "kind": "petri", + "id": "execution 0/25/0", + "recorded_at": 1789709597265, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 25, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "no", + "kind": "attractor/command", + "meta": { + "label": "no", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "4": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597265, + "record": { + "seq": 25, + "origin": "external", + "recorded_at": 1789709597265, + "body": { + "event": "routing.resolved", + "decision_id": { + "route": { + "firing": 3, + "attempt": 1 + } + }, + "groups": [ + { + "group": 0, + "draw": null, + "trace": [], + "decision": { + "emit": 4 + } + } + ] + } + }, + "derived": { + "groups": [ + { + "group": 0, + "target": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + } + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 55, + "kind": "petri", + "id": "execution 0/25/1", + "recorded_at": 1789709597265, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 25, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597265, + "derived": { + "event": "visit.started", + "inputs": [ + { + "edge": 4, + "generation": 0, + "payload": { + "exit_status": 0, + "stdout": "", + "outcome": "succeeded", + "failure_class": "" + }, + "from": 3 + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 56, + "kind": "petri", + "id": "execution 0/25/2", + "recorded_at": 1789709597265, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 25, + "index": 2 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597265, + "derived": { + "event": "wait.state.changed", + "state": "awaiting_admission" + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 57, + "kind": "petri", + "id": "execution 0/26/0", + "recorded_at": 1789709597265, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 26, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "no", + "kind": "attractor/command", + "meta": { + "label": "no", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "4": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597265, + "record": { + "seq": 26, + "origin": "core", + "recorded_at": 1789709597265, + "body": { + "event": "route.applied", + "kind": "edge", + "firing": 3, + "group": 0, + "edge": 4 + } + }, + "derived": { + "target": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "transition": "Continue", + "back": false + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 58, + "kind": "petri", + "id": "execution 0/27/0", + "recorded_at": 1789709597265, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 27, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "no", + "kind": "attractor/command", + "meta": { + "label": "no", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "4": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597265, + "record": { + "seq": 27, + "origin": "core", + "recorded_at": 1789709597265, + "body": { + "event": "token.emitted", + "edge": 4, + "generation": 0, + "payload": { + "exit_status": 0, + "stdout": "", + "outcome": "succeeded", + "failure_class": "" + }, + "from": 3 + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 59, + "kind": "petri", + "id": "execution 0/28/0", + "recorded_at": 1789709597265, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 28, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597265, + "record": { + "seq": 28, + "origin": "external", + "recorded_at": 1789709597265, + "body": { + "event": "admission.decided", + "decision_id": { + "attempt_start": { + "firing": 4, + "attempt": 1 + } + }, + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 60, + "kind": "petri", + "id": "execution 0/29/0", + "recorded_at": 1789709597265, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 29, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597265, + "record": { + "seq": 29, + "origin": "external", + "recorded_at": 1789709597265, + "body": { + "event": "step.started", + "firing": 4, + "attempt": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 61, + "kind": "petri", + "id": "execution 0/29/1", + "recorded_at": 1789709597265, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 29, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597265, + "derived": { + "event": "wait.state.changed", + "state": "running" + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 62, + "kind": "petri", + "id": "execution 0/30/0", + "recorded_at": 1789709597370, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 30, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597370, + "record": { + "seq": 30, + "origin": "external", + "recorded_at": 1789709597370, + "body": { + "event": "step.progress.recorded", + "firing": 4, + "ev": { + "custom": { + "$note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 4, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "49d77497eb2088a5a84c05fe03824f20621e3a3b", + "reused": false + } + } + } + } + } + }, + "derived": { + "parsed": { + "kind": "note", + "note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 4, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "49d77497eb2088a5a84c05fe03824f20621e3a3b", + "reused": false + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 63, + "kind": "petri", + "id": "execution 0/31/0", + "recorded_at": 1789709597370, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 31, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597370, + "record": { + "seq": 31, + "origin": "external", + "recorded_at": 1789709597370, + "body": { + "event": "step.finished", + "firing": 4, + "attempt": 1, + "outcome": { + "status": "success", + "output": { + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 0 + }, + "context_updates": { + "failure_class": "" + } + } + } + }, + "derived": { + "final": true, + "exhausted": false + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 64, + "kind": "petri", + "id": "execution 0/31/1", + "recorded_at": 1789709597370, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 31, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597370, + "derived": { + "event": "visit.completed", + "outcome": { + "status": "success", + "output": { + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 0 + }, + "context_updates": { + "failure_class": "" + } + }, + "executed": true, + "attempts": 1 + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 65, + "kind": "platform", + "id": "11", + "recorded_at": 1789709597370, + "item": { + "seq": 11, + "recorded_at": 1789709597370, + "record": { + "kind": "checkpoint", + "execution": 0, + "firing": 4, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "49d77497eb2088a5a84c05fe03824f20621e3a3b", + "operation": { + "execution": 0, + "decision": { + "attempt_start": { + "firing": 4, + "attempt": 1 + } + }, + "effect": "checkpoint" + } + }, + "position": { + "execution": 0, + "firing": 4 + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 66, + "kind": "petri", + "id": "coordinator/4/0", + "recorded_at": 1789709597371, + "item": { + "id": { + "log": "coordinator", + "seq": 4, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709597371, + "record": { + "seq": 4, + "origin": "external", + "recorded_at": 1789709597371, + "body": { + "event": "execution.finished", + "execution": 0, + "exit": { + "terminal": { + "status": "success" + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 67, + "kind": "petri", + "id": "execution 0/32/0", + "recorded_at": 1789709597371, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 32, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597371, + "record": { + "seq": 32, + "origin": "external", + "recorded_at": 1789709597371, + "body": { + "event": "routing.resolved", + "decision_id": { + "route": { + "firing": 4, + "attempt": 1 + } + }, + "groups": [] + } + }, + "derived": { + "groups": [] + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 68, + "kind": "petri", + "id": "coordinator/5/0", + "recorded_at": 1789709597371, + "item": { + "id": { + "log": "coordinator", + "seq": 5, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709597371, + "record": { + "seq": 5, + "origin": "external", + "recorded_at": 1789709597371, + "body": { + "event": "invocation.finished", + "invocation": 0, + "result": { + "status": "success", + "failure": null, + "final_execution": 0, + "output": null, + "context": { + "command.output": "", + "failure_class": "", + "human.gate.gate.answer": "no", + "human.gate.gate.label": "[N] No", + "human.gate.gate.question": "Go?", + "human.gate.label": "[N] No", + "human.gate.selected": "N", + "internal.run_id": "petri" + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 69, + "kind": "petri", + "id": "coordinator/6/0", + "recorded_at": 1789709597412, + "item": { + "id": { + "log": "coordinator", + "seq": 6, + "index": 0 + }, + "origin": "external", + "context": {}, + "recorded_at": 1789709597412, + "record": { + "seq": 6, + "origin": "external", + "recorded_at": 1789709597412, + "body": { + "event": "run.finished", + "status": "success" + } + } + } + }, + { + "run_id": "01M2SG2E0BEBVXSJCT8MZE19FT", + "stream_seq": 70, + "kind": "platform", + "id": "12", + "recorded_at": 1789709597416, + "item": { + "seq": 12, + "recorded_at": 1789709597416, + "record": { + "kind": "run.lifecycle", + "transition": "succeeded", + "status": { + "kind": "succeeded", + "reason": "completed" + } + } + } + } + ] +} \ No newline at end of file diff --git a/apps/fabro-web/app/test-fixtures/petri/hello.json b/apps/fabro-web/app/test-fixtures/petri/hello.json new file mode 100644 index 000000000..31eedc4c3 --- /dev/null +++ b/apps/fabro-web/app/test-fixtures/petri/hello.json @@ -0,0 +1,4792 @@ +{ + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "projection": { + "title": "Say hello and demonstrate a basic Fabro workflow", + "spec": { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "settings": { + "project": { + "name": null, + "description": null, + "metadata": {} + }, + "workflow": { + "name": null, + "description": null, + "graph": "workflow.fabro", + "metadata": {}, + "engine": "petri" + }, + "run": { + "goal": null, + "working_dir": null, + "metadata": {}, + "inputs": {}, + "model": { + "provider": null, + "name": "gpt-5.4", + "fallbacks": {}, + "controls": { + "reasoning_effort": null, + "speed": null + } + }, + "git": { + "author": null + }, + "prepare": { + "steps": [], + "timeout_ms": 300000 + }, + "execution": { + "mode": "normal", + "approval": "prompt" + }, + "checkpoint": { + "exclude_globs": [], + "skip_git_hooks": false, + "commit_timeout_ms": 30000 + }, + "clone": { + "enabled": true, + "depth": 100 + }, + "run_branch": { + "enabled": true, + "push": true + }, + "environment": { + "id": "local", + "provider": "local", + "image": { + "docker": null, + "dockerfile": null + }, + "resources": { + "cpu": null, + "memory": null, + "disk": null + }, + "network": { + "mode": "allow_all", + "allow": [] + }, + "lifecycle": { + "preserve": false, + "stop_on_terminal": true, + "auto_stop": null + }, + "labels": {}, + "env": {} + }, + "notifications": {}, + "interviews": { + "provider": null, + "slack": null + }, + "agent": { + "fabro_tools": false, + "mcps": {} + }, + "hooks": [], + "scm": { + "provider": null, + "owner": null, + "repository": null, + "github": null + }, + "pull_request": null, + "artifacts": { + "include": [] + }, + "integrations": { + "github": { + "permissions": {} + } + } + } + }, + "graph": { + "name": "Hello", + "nodes": { + "start": { + "id": "start", + "attrs": { + "label": { + "String": "Start" + }, + "shape": { + "String": "Mdiamond" + } + } + }, + "exit": { + "id": "exit", + "attrs": { + "label": { + "String": "Exit" + }, + "shape": { + "String": "Msquare" + } + } + }, + "greet": { + "id": "greet", + "attrs": { + "prompt": { + "String": "Add a haiku to the README" + }, + "label": { + "String": "Greet" + } + } + } + }, + "edges": [ + { + "from": "start", + "to": "greet", + "attrs": {} + }, + { + "from": "greet", + "to": "exit", + "attrs": {} + } + ], + "attrs": { + "goal": { + "String": "Say hello and demonstrate a basic Fabro workflow" + }, + "rankdir": { + "String": "LR" + } + } + }, + "graph_source": "digraph Hello {\n graph [goal=\"Say hello and demonstrate a basic Fabro workflow\"]\n rankdir=LR\n\n start [shape=Mdiamond, label=\"Start\"]\n exit [shape=Msquare, label=\"Exit\"]\n\n greet [label=\"Greet\", prompt=\"Add a haiku to the README\"]\n\n start -> greet -> exit\n}\n", + "workflow_slug": "workflow", + "workflow_version_id": "170dd4ba80b17475c7c2ad832fe0765d26ed889b45f4c923e8cc590fa56ceee2", + "target": { + "kind": "folder", + "path": "/private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpMzvKN4" + }, + "source_directory": "/private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpMzvKN4", + "provenance": { + "server": { + "version": "0.357.0-nightly.0" + }, + "subject": { + "kind": "user", + "identity": { + "issuer": "fabro:dev", + "subject": "dev" + }, + "login": "dev", + "auth_method": "dev_token" + } + }, + "spec_blob": "f47d0e4c48a1e5ee5ec9dcc377e494e95f1b079cff233a850cd7483cce99864b", + "engine": { + "kind": "petri", + "graph": { + "blob": "11a2b8697a0f230a5e2e764ee5540136c443c8336479e1ad89f7dc6e502eb44f", + "digest": "11a2b8697a0f230a5e2e764ee5540136c443c8336479e1ad89f7dc6e502eb44f" + } + } + }, + "web_url": "http://localhost:3000/runs/01M2SG2E0BHHDQEF0ZKBQXR921", + "start": { + "start_time": "2026-09-18T05:33:16.741Z" + }, + "status": { + "kind": "succeeded", + "reason": "completed" + }, + "status_updated_at": "2026-09-18T05:33:17.534Z", + "last_event_at": "2026-09-18T05:33:17.538Z", + "pending_control": null, + "checkpoints": [ + { + "seq": 28, + "checkpoint": { + "timestamp": "2026-09-18T05:33:17.013Z", + "current_node": "start", + "completed_nodes": [], + "node_retries": {}, + "context_values": {}, + "git_commit_sha": "4e6cae6457c67dff216f14eaa83d942a15dd49a0" + }, + "diff": {} + }, + { + "seq": 57, + "checkpoint": { + "timestamp": "2026-09-18T05:33:17.390Z", + "current_node": "greet", + "completed_nodes": [], + "node_retries": {}, + "context_values": {}, + "git_commit_sha": "577da173f630270470d5b7e484b0b4838f6e2506" + }, + "diff": {} + }, + { + "seq": 61, + "checkpoint": { + "timestamp": "2026-09-18T05:33:17.492Z", + "current_node": "exit", + "completed_nodes": [], + "node_retries": {}, + "context_values": {}, + "git_commit_sha": "fc290eb55d3149ca2be57b3e39ee609904a5663d" + }, + "diff": {} + } + ], + "conclusion": { + "timestamp": "2026-09-18T05:33:17.534Z", + "status": "succeeded", + "timing": { + "wall_time_ms": 793, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, + "final_git_commit_sha": "fc290eb55d3149ca2be57b3e39ee609904a5663d", + "usage": { + "tokens": { + "input": 1, + "output": 5, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + }, + "cost": { + "usd_micros": 77, + "source": "catalog" + } + }, + "total_retries": 0, + "diff": {} + }, + "sandbox": { + "kind": "planned", + "plan": { + "provider": "local" + } + }, + "pull_request": null, + "superseded_by": null, + "pending_interviews": {}, + "stages": { + "greet@1": { + "first_event_seq": 331, + "prompt": null, + "response": "A haiku, added.", + "completion": { + "outcome": "succeeded", + "notes": null, + "failure_reason": null, + "timestamp": "2026-09-18T05:33:17.390Z" + }, + "provider_used": { + "mode": "agent", + "provider": "openai", + "model": "gpt-5.4" + }, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "output": "A haiku, added.\n", + "output_bytes": 16, + "live_streaming": false, + "termination": "exited", + "started_at": "2026-09-18T05:33:17.013Z", + "handler": "agent", + "graph_visit": 1, + "timing": { + "wall_time_ms": 268, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, + "usage": { + "tokens": { + "input": 1, + "output": 5, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + }, + "cost": { + "usd_micros": 77, + "source": "catalog" + } + }, + "model": { + "provider": "openai", + "model_id": "gpt-5.4" + }, + "agent": { + "root_session_id": "ses_951f513e-ed1c-4c82-ad49-be68d093de09", + "route": { + "provider": "openai", + "model": "gpt-5.4" + }, + "activity": "ended", + "usage": { + "tokens": { + "input": 1, + "output": 5, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + }, + "cost": { + "usd_micros": 77, + "source": "catalog" + } + }, + "messages": 1, + "descendants": {}, + "context_window": { + "provider": "openai", + "model": "gpt-5.4", + "context_window_tokens": 1050000, + "input_tokens": 1, + "usage_percent": 0.00009523809523809524, + "count_method": "response_usage_scaled_breakdown", + "staleness": "live", + "generated_at": "2026-09-18T05:33:17.281Z", + "breakdown": [ + { + "category": "system_prompt", + "tokens": 0, + "usage_percent": 0.0 + }, + { + "category": "tools", + "tokens": 0, + "usage_percent": 0.0 + }, + { + "category": "conversation", + "tokens": 0, + "usage_percent": 0.0 + }, + { + "category": "other", + "tokens": 1, + "usage_percent": 0.00009523809523809524 + } + ], + "warnings": [] + }, + "tools": {}, + "retries": 0, + "mcp_servers": {}, + "skills": { + "available": [], + "activated": [] + }, + "subagent_counts": { + "spawned": 0, + "turns_started": 0, + "completed": 0, + "failed": 0, + "closed": 0 + }, + "todos": {}, + "subagents": [], + "compactions": [], + "failovers": [], + "files_touched": [], + "last_file_touched": null, + "prompts": 1, + "prompt": { + "completed": true, + "usage": { + "tokens": { + "input": 1, + "output": 5, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + }, + "cost": { + "usd_micros": 77, + "source": "catalog" + } + }, + "messages": 1, + "context_window": { + "provider": "openai", + "model": "gpt-5.4", + "context_window_tokens": 1050000, + "input_tokens": 1, + "usage_percent": 0.00009523809523809524, + "count_method": "response_usage_scaled_breakdown", + "staleness": "live", + "generated_at": "2026-09-18T05:33:17.281Z", + "breakdown": [ + { + "category": "system_prompt", + "tokens": 0, + "usage_percent": 0.0 + }, + { + "category": "tools", + "tokens": 0, + "usage_percent": 0.0 + }, + { + "category": "conversation", + "tokens": 0, + "usage_percent": 0.0 + }, + { + "category": "other", + "tokens": 1, + "usage_percent": 0.00009523809523809524 + } + ], + "warnings": [] + }, + "tool_calls": 0, + "retries": 0, + "failovers": 0, + "descendants": {}, + "subagents": { + "spawned": 0, + "turns_started": 0, + "completed": 0, + "failed": 0, + "closed": 0 + }, + "compactions": [], + "files_touched": [], + "last_file_touched": null + } + }, + "state": "succeeded" + }, + "exit@1": { + "first_event_seq": 708, + "prompt": null, + "response": null, + "completion": { + "outcome": "succeeded", + "notes": null, + "failure_reason": null, + "timestamp": "2026-09-18T05:33:17.492Z" + }, + "provider_used": null, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "output": null, + "live_streaming": false, + "termination": "exited", + "started_at": "2026-09-18T05:33:17.390Z", + "handler": "exit", + "graph_visit": 1, + "timing": { + "wall_time_ms": 0, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, + "usage": { + "tokens": { + "input": 0, + "output": 0, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + } + }, + "state": "succeeded" + }, + "start@1": { + "first_event_seq": 61, + "prompt": null, + "response": null, + "completion": { + "outcome": "succeeded", + "notes": null, + "failure_reason": null, + "timestamp": "2026-09-18T05:33:17.013Z" + }, + "provider_used": null, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "output": "checkout: /private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpMzvKN4 is not a Git repository; the workspace starts empty\n", + "output_bytes": 130, + "live_streaming": false, + "termination": "exited", + "started_at": "2026-09-18T05:33:16.743Z", + "handler": "start", + "graph_visit": 1, + "timing": { + "wall_time_ms": 9, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, + "usage": { + "tokens": { + "input": 0, + "output": 0, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + } + }, + "state": "succeeded" + } + } + }, + "stream": [ + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 1, + "kind": "platform", + "id": "1", + "recorded_at": 1789709596683, + "item": { + "seq": 1, + "recorded_at": 1789709596683, + "record": { + "kind": "run.created", + "spec": { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "settings": { + "project": { + "name": null, + "description": null, + "metadata": {} + }, + "workflow": { + "name": null, + "description": null, + "graph": "workflow.fabro", + "metadata": {}, + "engine": "petri" + }, + "run": { + "goal": null, + "working_dir": null, + "metadata": {}, + "inputs": {}, + "model": { + "provider": null, + "name": "gpt-5.4", + "fallbacks": {}, + "controls": { + "reasoning_effort": null, + "speed": null + } + }, + "git": { + "author": null + }, + "prepare": { + "steps": [], + "timeout_ms": 300000 + }, + "execution": { + "mode": "normal", + "approval": "prompt" + }, + "checkpoint": { + "exclude_globs": [], + "skip_git_hooks": false, + "commit_timeout_ms": 30000 + }, + "clone": { + "enabled": true, + "depth": 100 + }, + "run_branch": { + "enabled": true, + "push": true + }, + "environment": { + "id": "local", + "provider": "local", + "image": { + "docker": null, + "dockerfile": null + }, + "resources": { + "cpu": null, + "memory": null, + "disk": null + }, + "network": { + "mode": "allow_all", + "allow": [] + }, + "lifecycle": { + "preserve": false, + "stop_on_terminal": true, + "auto_stop": null + }, + "labels": {}, + "env": {} + }, + "notifications": {}, + "interviews": { + "provider": null, + "slack": null + }, + "agent": { + "fabro_tools": false, + "mcps": {} + }, + "hooks": [], + "scm": { + "provider": null, + "owner": null, + "repository": null, + "github": null + }, + "pull_request": null, + "artifacts": { + "include": [] + }, + "integrations": { + "github": { + "permissions": {} + } + } + } + }, + "graph": { + "name": "Hello", + "nodes": { + "start": { + "id": "start", + "attrs": { + "shape": { + "String": "Mdiamond" + }, + "label": { + "String": "Start" + } + } + }, + "greet": { + "id": "greet", + "attrs": { + "label": { + "String": "Greet" + }, + "prompt": { + "String": "Add a haiku to the README" + } + } + }, + "exit": { + "id": "exit", + "attrs": { + "label": { + "String": "Exit" + }, + "shape": { + "String": "Msquare" + } + } + } + }, + "edges": [ + { + "from": "start", + "to": "greet", + "attrs": {} + }, + { + "from": "greet", + "to": "exit", + "attrs": {} + } + ], + "attrs": { + "rankdir": { + "String": "LR" + }, + "goal": { + "String": "Say hello and demonstrate a basic Fabro workflow" + } + } + }, + "graph_source": "digraph Hello {\n graph [goal=\"Say hello and demonstrate a basic Fabro workflow\"]\n rankdir=LR\n\n start [shape=Mdiamond, label=\"Start\"]\n exit [shape=Msquare, label=\"Exit\"]\n\n greet [label=\"Greet\", prompt=\"Add a haiku to the README\"]\n\n start -> greet -> exit\n}\n", + "workflow_slug": "workflow", + "workflow_version_id": "170dd4ba80b17475c7c2ad832fe0765d26ed889b45f4c923e8cc590fa56ceee2", + "target": { + "kind": "folder", + "path": "/private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpMzvKN4" + }, + "source_directory": "/private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpMzvKN4", + "provenance": { + "server": { + "version": "0.357.0-nightly.0" + }, + "subject": { + "kind": "user", + "identity": { + "issuer": "fabro:dev", + "subject": "dev" + }, + "login": "dev", + "auth_method": "dev_token" + } + }, + "spec_blob": "f47d0e4c48a1e5ee5ec9dcc377e494e95f1b079cff233a850cd7483cce99864b", + "engine": { + "kind": "petri", + "graph": { + "blob": "11a2b8697a0f230a5e2e764ee5540136c443c8336479e1ad89f7dc6e502eb44f", + "digest": "11a2b8697a0f230a5e2e764ee5540136c443c8336479e1ad89f7dc6e502eb44f" + } + } + }, + "title": "Say hello and demonstrate a basic Fabro workflow", + "web_url": "http://localhost:3000/runs/01M2SG2E0BHHDQEF0ZKBQXR921" + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 2, + "kind": "platform", + "id": "2", + "recorded_at": 1789709596731, + "item": { + "seq": 2, + "recorded_at": 1789709596731, + "record": { + "kind": "run.lifecycle", + "transition": "submitted", + "status": { + "kind": "submitted" + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 3, + "kind": "platform", + "id": "3", + "recorded_at": 1789709596736, + "item": { + "seq": 3, + "recorded_at": 1789709596736, + "record": { + "kind": "run.lifecycle", + "transition": "start_requested", + "source": "start" + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 4, + "kind": "platform", + "id": "4", + "recorded_at": 1789709596736, + "item": { + "seq": 4, + "recorded_at": 1789709596736, + "record": { + "kind": "run.lifecycle", + "transition": "runnable", + "status": { + "kind": "runnable" + }, + "source": "start_requested" + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 5, + "kind": "platform", + "id": "5", + "recorded_at": 1789709596738, + "item": { + "seq": 5, + "recorded_at": 1789709596738, + "record": { + "kind": "run.lifecycle", + "transition": "starting", + "status": { + "kind": "starting" + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 6, + "kind": "platform", + "id": "6", + "recorded_at": 1789709596739, + "item": { + "seq": 6, + "recorded_at": 1789709596739, + "record": { + "kind": "run.lifecycle", + "transition": "running", + "status": { + "kind": "running" + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 7, + "kind": "petri", + "id": "coordinator/0/0", + "recorded_at": 1789709596741, + "item": { + "id": { + "log": "coordinator", + "seq": 0, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0 + }, + "recorded_at": 1789709596741, + "record": { + "seq": 0, + "origin": "external", + "recorded_at": 1789709596741, + "body": { + "event": "run.started", + "format_version": 5, + "key": "01M2SG2E0BHHDQEF0ZKBQXR921", + "root": 0, + "middleware_chain": [ + "circuit-breaker" + ] + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 8, + "kind": "petri", + "id": "coordinator/1/0", + "recorded_at": 1789709596742, + "item": { + "id": { + "log": "coordinator", + "seq": 1, + "index": 0 + }, + "origin": "external", + "context": {}, + "recorded_at": 1789709596742, + "record": { + "seq": 1, + "origin": "external", + "recorded_at": 1789709596742, + "body": { + "event": "graph.registered", + "digest": "11a2b8697a0f230a5e2e764ee5540136c443c8336479e1ad89f7dc6e502eb44f" + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 9, + "kind": "petri", + "id": "coordinator/2/0", + "recorded_at": 1789709596742, + "item": { + "id": { + "log": "coordinator", + "seq": 2, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0 + }, + "recorded_at": 1789709596742, + "record": { + "seq": 2, + "origin": "external", + "recorded_at": 1789709596742, + "body": { + "event": "invocation.declared", + "invocation": 0, + "call": null, + "graph": "11a2b8697a0f230a5e2e764ee5540136c443c8336479e1ad89f7dc6e502eb44f", + "context": {}, + "secret_bindings": "none", + "sandbox": "isolated" + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 10, + "kind": "petri", + "id": "coordinator/3/0", + "recorded_at": 1789709596742, + "item": { + "id": { + "log": "coordinator", + "seq": 3, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709596742, + "record": { + "seq": 3, + "origin": "external", + "recorded_at": 1789709596742, + "body": { + "event": "execution.declared", + "execution": 0, + "invocation": 0, + "predecessor": null, + "start": { + "entry": "graph_entries", + "context": {}, + "prior_firings": {}, + "execution_index": 0, + "max_executions": 32 + }, + "middleware_state": { + "circuit-breaker": [ + 1, + { + "loop_signatures": {}, + "restart_signatures": {}, + "pending": {} + } + ] + } + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 11, + "kind": "petri", + "id": "execution 0/0/0", + "recorded_at": 1789709596743, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 0, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709596743, + "record": { + "seq": 0, + "origin": "external", + "recorded_at": 1789709596743, + "body": { + "event": "execution.started", + "entry": "graph_entries", + "context": {}, + "prior_firings": {}, + "execution_index": 0, + "max_executions": 32 + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 12, + "kind": "petri", + "id": "execution 0/1/0", + "recorded_at": 1789709596743, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 1, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709596743, + "record": { + "seq": 1, + "origin": "external", + "recorded_at": 1789709596743, + "body": { + "event": "admission.decided", + "decision_id": "execution_start", + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 13, + "kind": "petri", + "id": "execution 0/1/1", + "recorded_at": 1789709596743, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 1, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "Start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "greet", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596743, + "derived": { + "event": "visit.started", + "inputs": [ + { + "edge": 2, + "generation": 0, + "payload": null, + "from": 0 + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 14, + "kind": "petri", + "id": "execution 0/1/2", + "recorded_at": 1789709596743, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 1, + "index": 2 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "Start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "greet", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596743, + "derived": { + "event": "wait.state.changed", + "state": "awaiting_admission" + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 15, + "kind": "petri", + "id": "execution 0/2/0", + "recorded_at": 1789709596743, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 2, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709596743, + "record": { + "seq": 2, + "origin": "core", + "recorded_at": 1789709596743, + "body": { + "event": "token.emitted", + "edge": 2, + "generation": 0, + "payload": null, + "from": 0 + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 16, + "kind": "petri", + "id": "execution 0/3/0", + "recorded_at": 1789709596743, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 3, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "Start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "greet", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596743, + "record": { + "seq": 3, + "origin": "external", + "recorded_at": 1789709596743, + "body": { + "event": "admission.decided", + "decision_id": { + "attempt_start": { + "firing": 1, + "attempt": 1 + } + }, + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 17, + "kind": "petri", + "id": "execution 0/4/0", + "recorded_at": 1789709596829, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 4, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "Start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "greet", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596829, + "record": { + "seq": 4, + "origin": "external", + "recorded_at": 1789709596829, + "body": { + "event": "step.started", + "firing": 1, + "attempt": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 18, + "kind": "petri", + "id": "execution 0/4/1", + "recorded_at": 1789709596829, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 4, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "Start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "greet", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596829, + "derived": { + "event": "wait.state.changed", + "state": "running" + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 19, + "kind": "petri", + "id": "execution 0/5/0", + "recorded_at": 1789709596838, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 5, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "Start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "greet", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596838, + "record": { + "seq": 5, + "origin": "external", + "recorded_at": 1789709596838, + "body": { + "event": "step.progress.recorded", + "firing": 1, + "ev": { + "log": { + "stream": "stderr", + "line": "checkout: /private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpMzvKN4 is not a Git repository; the workspace starts empty" + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 20, + "kind": "petri", + "id": "execution 0/6/0", + "recorded_at": 1789709597012, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 6, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "Start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "greet", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597012, + "record": { + "seq": 6, + "origin": "external", + "recorded_at": 1789709597012, + "body": { + "event": "step.progress.recorded", + "firing": 1, + "ev": { + "custom": { + "$note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 1, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "4e6cae6457c67dff216f14eaa83d942a15dd49a0", + "reused": false + } + } + } + } + } + }, + "derived": { + "parsed": { + "kind": "note", + "note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 1, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "4e6cae6457c67dff216f14eaa83d942a15dd49a0", + "reused": false + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 21, + "kind": "petri", + "id": "execution 0/7/0", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 7, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "Start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "greet", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "record": { + "seq": 7, + "origin": "external", + "recorded_at": 1789709597013, + "body": { + "event": "step.finished", + "firing": 1, + "attempt": 1, + "outcome": { + "status": "success", + "output": { + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 9 + }, + "context_updates": { + "failure_class": "", + "internal.run_id": "petri" + } + } + } + }, + "derived": { + "final": true, + "exhausted": false + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 22, + "kind": "petri", + "id": "execution 0/7/1", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 7, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "Start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "greet", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "derived": { + "event": "visit.completed", + "outcome": { + "status": "success", + "output": { + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 9 + }, + "context_updates": { + "failure_class": "", + "internal.run_id": "petri" + } + }, + "executed": true, + "attempts": 1 + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 23, + "kind": "petri", + "id": "execution 0/8/0", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 8, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "Start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "greet", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "record": { + "seq": 8, + "origin": "external", + "recorded_at": 1789709597013, + "body": { + "event": "routing.resolved", + "decision_id": { + "route": { + "firing": 1, + "attempt": 1 + } + }, + "groups": [ + { + "group": 0, + "draw": null, + "trace": [], + "decision": { + "emit": 0 + } + } + ] + } + }, + "derived": { + "groups": [ + { + "group": 0, + "target": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + } + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 24, + "kind": "petri", + "id": "execution 0/8/1", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 8, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "derived": { + "event": "visit.started", + "inputs": [ + { + "edge": 0, + "generation": 0, + "payload": { + "from": "start", + "fidelity": null, + "thread_id": null + }, + "from": 1 + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 25, + "kind": "petri", + "id": "execution 0/8/2", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 8, + "index": 2 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "derived": { + "event": "wait.state.changed", + "state": "awaiting_admission" + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 26, + "kind": "petri", + "id": "execution 0/9/0", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 9, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "Start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "greet", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "record": { + "seq": 9, + "origin": "core", + "recorded_at": 1789709597013, + "body": { + "event": "route.applied", + "kind": "edge", + "firing": 1, + "group": 0, + "edge": 0 + } + }, + "derived": { + "target": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "transition": "Continue", + "back": false + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 27, + "kind": "petri", + "id": "execution 0/10/0", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 10, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "Start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 5, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "greet", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "record": { + "seq": 10, + "origin": "core", + "recorded_at": 1789709597013, + "body": { + "event": "token.emitted", + "edge": 0, + "generation": 0, + "payload": { + "from": "start", + "fidelity": null, + "thread_id": null + }, + "from": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 28, + "kind": "platform", + "id": "7", + "recorded_at": 1789709597013, + "item": { + "seq": 7, + "recorded_at": 1789709597013, + "record": { + "kind": "checkpoint", + "execution": 0, + "firing": 1, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "4e6cae6457c67dff216f14eaa83d942a15dd49a0", + "operation": { + "execution": 0, + "decision": { + "attempt_start": { + "firing": 1, + "attempt": 1 + } + }, + "effect": "checkpoint" + } + }, + "position": { + "execution": 0, + "firing": 1 + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 29, + "kind": "petri", + "id": "execution 0/11/0", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 11, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "record": { + "seq": 11, + "origin": "external", + "recorded_at": 1789709597013, + "body": { + "event": "admission.decided", + "decision_id": { + "attempt_start": { + "firing": 2, + "attempt": 1 + } + }, + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 30, + "kind": "petri", + "id": "execution 0/12/0", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 12, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "record": { + "seq": 12, + "origin": "external", + "recorded_at": 1789709597013, + "body": { + "event": "step.started", + "firing": 2, + "attempt": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 31, + "kind": "petri", + "id": "execution 0/12/1", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 12, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "derived": { + "event": "wait.state.changed", + "state": "running" + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 32, + "kind": "petri", + "id": "execution 0/13/0", + "recorded_at": 1789709597014, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 13, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597014, + "record": { + "seq": 13, + "origin": "external", + "recorded_at": 1789709597014, + "body": { + "event": "step.progress.recorded", + "firing": 2, + "ev": { + "custom": { + "kind": "attractor.thread", + "node": "greet", + "firing": 2, + "attempt": 1, + "fidelity": "compact", + "fidelity_source": "default", + "thread": "start", + "thread_source": "previous", + "reused": false, + "backend": "api" + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 33, + "kind": "petri", + "id": "execution 0/14/0", + "recorded_at": 1789709597014, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 14, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597014, + "record": { + "seq": 14, + "origin": "external", + "recorded_at": 1789709597014, + "body": { + "event": "step.progress.recorded", + "firing": 2, + "ev": { + "custom": { + "kind": "attractor.fallback.plan", + "node": "greet", + "firing": 2, + "attempt": 1, + "requested": { + "provider": "openai", + "model": "gpt-5.4" + }, + "routes": [ + { + "position": 0, + "provider": "openai", + "model": "gpt-5.4", + "reasoning_effort": null, + "speed": null + } + ], + "notices": [] + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 34, + "kind": "petri", + "id": "execution 0/15/0", + "recorded_at": 1789709597052, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 15, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597052, + "record": { + "seq": 15, + "origin": "external", + "recorded_at": 1789709597052, + "body": { + "event": "step.progress.recorded", + "firing": 2, + "ev": { + "custom": { + "kind": "pebble", + "firing": 2, + "attempt": 1, + "scope": 0, + "node": "greet", + "event": { + "seq": 1, + "stream_id": "ses_951f513e-ed1c-4c82-ad49-be68d093de09", + "event": { + "SessionStarted": { + "provider": "openai", + "model": "gpt-5.4" + } + }, + "timestamp": "2026-09-18T05:33:17.052Z", + "session_id": "ses_951f513e-ed1c-4c82-ad49-be68d093de09" + } + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 35, + "kind": "petri", + "id": "execution 0/16/0", + "recorded_at": 1789709597195, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 16, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597195, + "record": { + "seq": 16, + "origin": "external", + "recorded_at": 1789709597195, + "body": { + "event": "step.progress.recorded", + "firing": 2, + "ev": { + "custom": { + "kind": "pebble", + "firing": 2, + "attempt": 1, + "scope": 0, + "node": "greet", + "event": { + "seq": 2, + "stream_id": "ses_951f513e-ed1c-4c82-ad49-be68d093de09", + "event": { + "MemoryLoaded": { + "profile": "openai", + "files": [], + "total_loaded_bytes": 0, + "budget_bytes": 32768 + } + }, + "timestamp": "2026-09-18T05:33:17.194Z", + "session_id": "ses_951f513e-ed1c-4c82-ad49-be68d093de09" + } + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 36, + "kind": "petri", + "id": "execution 0/17/0", + "recorded_at": 1789709597195, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 17, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597195, + "record": { + "seq": 17, + "origin": "external", + "recorded_at": 1789709597195, + "body": { + "event": "step.progress.recorded", + "firing": 2, + "ev": { + "custom": { + "kind": "pebble", + "firing": 2, + "attempt": 1, + "scope": 0, + "node": "greet", + "event": { + "seq": 3, + "stream_id": "ses_951f513e-ed1c-4c82-ad49-be68d093de09", + "event": { + "SkillsDiscovered": { + "profile": "openai", + "source_dirs": [ + "/Users/bhelmkamp/.fabro/skills", + "/private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/fabro-test-01M2SG2DYCTTKW929NH2KXTGK1/storage/scratch/20260918-01M2SG2E0BHHDQEF0ZKBQXR921/petri/scopes/invocation-0-scope-0/work/.fabro/skills", + "/private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/fabro-test-01M2SG2DYCTTKW929NH2KXTGK1/storage/scratch/20260918-01M2SG2E0BHHDQEF0ZKBQXR921/petri/scopes/invocation-0-scope-0/work/skills" + ], + "skills": [], + "skipped": [] + } + }, + "timestamp": "2026-09-18T05:33:17.194Z", + "session_id": "ses_951f513e-ed1c-4c82-ad49-be68d093de09" + } + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 37, + "kind": "petri", + "id": "execution 0/18/0", + "recorded_at": 1789709597195, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 18, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597195, + "record": { + "seq": 18, + "origin": "external", + "recorded_at": 1789709597195, + "body": { + "event": "step.progress.recorded", + "firing": 2, + "ev": { + "custom": { + "kind": "attractor.skills", + "node": "greet", + "firing": 2, + "attempt": 1, + "scope": 0, + "dirs": [ + { + "path": "/Users/bhelmkamp/.fabro/skills", + "source": "configured" + }, + { + "path": "/private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/fabro-test-01M2SG2DYCTTKW929NH2KXTGK1/storage/scratch/20260918-01M2SG2E0BHHDQEF0ZKBQXR921/petri/scopes/invocation-0-scope-0/work/.fabro/skills", + "source": "project_fabro" + }, + { + "path": "/private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/fabro-test-01M2SG2DYCTTKW929NH2KXTGK1/storage/scratch/20260918-01M2SG2E0BHHDQEF0ZKBQXR921/petri/scopes/invocation-0-scope-0/work/skills", + "source": "project" + } + ] + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 38, + "kind": "petri", + "id": "execution 0/19/0", + "recorded_at": 1789709597280, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 19, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597280, + "record": { + "seq": 19, + "origin": "external", + "recorded_at": 1789709597280, + "body": { + "event": "step.progress.recorded", + "firing": 2, + "ev": { + "custom": { + "kind": "pebble", + "firing": 2, + "attempt": 1, + "scope": 0, + "node": "greet", + "event": { + "seq": 4, + "stream_id": "ses_951f513e-ed1c-4c82-ad49-be68d093de09", + "event": { + "UserInput": { + "text": "Goal: Say hello and demonstrate a basic Fabro workflow\n\n\nAdd a haiku to the README" + } + }, + "timestamp": "2026-09-18T05:33:17.280Z", + "session_id": "ses_951f513e-ed1c-4c82-ad49-be68d093de09" + } + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 39, + "kind": "petri", + "id": "execution 0/20/0", + "recorded_at": 1789709597280, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 20, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597280, + "record": { + "seq": 20, + "origin": "external", + "recorded_at": 1789709597280, + "body": { + "event": "step.progress.recorded", + "firing": 2, + "ev": { + "custom": { + "kind": "pebble", + "firing": 2, + "attempt": 1, + "scope": 0, + "node": "greet", + "event": { + "seq": 5, + "stream_id": "ses_951f513e-ed1c-4c82-ad49-be68d093de09", + "event": { + "LlmRequestStarted": { + "requested_model": "gpt-5.4" + } + }, + "timestamp": "2026-09-18T05:33:17.280Z", + "session_id": "ses_951f513e-ed1c-4c82-ad49-be68d093de09" + } + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 40, + "kind": "petri", + "id": "execution 0/21/0", + "recorded_at": 1789709597281, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 21, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597281, + "record": { + "seq": 21, + "origin": "external", + "recorded_at": 1789709597281, + "body": { + "event": "step.progress.recorded", + "firing": 2, + "ev": { + "custom": { + "kind": "pebble", + "firing": 2, + "attempt": 1, + "scope": 0, + "node": "greet", + "event": { + "seq": 6, + "stream_id": "ses_951f513e-ed1c-4c82-ad49-be68d093de09", + "event": { + "LlmFirstOutput": { + "kind": "text" + } + }, + "timestamp": "2026-09-18T05:33:17.281Z", + "session_id": "ses_951f513e-ed1c-4c82-ad49-be68d093de09" + } + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 41, + "kind": "petri", + "id": "execution 0/22/0", + "recorded_at": 1789709597281, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 22, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597281, + "record": { + "seq": 22, + "origin": "external", + "recorded_at": 1789709597281, + "body": { + "event": "step.progress.recorded", + "firing": 2, + "ev": { + "custom": { + "kind": "pebble", + "firing": 2, + "attempt": 1, + "scope": 0, + "node": "greet", + "event": { + "seq": 7, + "stream_id": "ses_951f513e-ed1c-4c82-ad49-be68d093de09", + "event": { + "TextDelta": { + "delta": "A haiku, added." + } + }, + "timestamp": "2026-09-18T05:33:17.281Z", + "session_id": "ses_951f513e-ed1c-4c82-ad49-be68d093de09" + } + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 42, + "kind": "petri", + "id": "execution 0/23/0", + "recorded_at": 1789709597282, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 23, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597282, + "record": { + "seq": 23, + "origin": "external", + "recorded_at": 1789709597282, + "body": { + "event": "step.progress.recorded", + "firing": 2, + "ev": { + "custom": { + "kind": "pebble", + "firing": 2, + "attempt": 1, + "scope": 0, + "node": "greet", + "event": { + "seq": 8, + "stream_id": "ses_951f513e-ed1c-4c82-ad49-be68d093de09", + "event": { + "AssistantMessage": { + "text": "A haiku, added.", + "model": "gpt-5.4", + "usage": { + "tokens": { + "input": 1, + "output": 5, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + }, + "cost": { + "usd_micros": 77, + "source": "catalog" + } + }, + "tool_call_count": 0, + "context_window": { + "provider": "openai", + "model": "gpt-5.4", + "context_window_tokens": 1050000, + "input_tokens": 1, + "usage_percent": 0.00009523809523809524, + "count_method": "response_usage_scaled_breakdown", + "staleness": "live", + "generated_at": "2026-09-18T05:33:17.281Z", + "breakdown": [ + { + "category": "system_prompt", + "tokens": 0, + "usage_percent": 0.0 + }, + { + "category": "tools", + "tokens": 0, + "usage_percent": 0.0 + }, + { + "category": "conversation", + "tokens": 0, + "usage_percent": 0.0 + }, + { + "category": "other", + "tokens": 1, + "usage_percent": 0.00009523809523809524 + } + ], + "warnings": [] + } + } + }, + "timestamp": "2026-09-18T05:33:17.281Z", + "session_id": "ses_951f513e-ed1c-4c82-ad49-be68d093de09" + } + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 43, + "kind": "petri", + "id": "execution 0/24/0", + "recorded_at": 1789709597282, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 24, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597282, + "record": { + "seq": 24, + "origin": "external", + "recorded_at": 1789709597282, + "body": { + "event": "step.progress.recorded", + "firing": 2, + "ev": { + "custom": { + "kind": "pebble", + "firing": 2, + "attempt": 1, + "scope": 0, + "node": "greet", + "event": { + "seq": 9, + "stream_id": "ses_951f513e-ed1c-4c82-ad49-be68d093de09", + "event": "ProcessingEnd", + "timestamp": "2026-09-18T05:33:17.282Z", + "session_id": "ses_951f513e-ed1c-4c82-ad49-be68d093de09" + } + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 44, + "kind": "petri", + "id": "execution 0/25/0", + "recorded_at": 1789709597282, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 25, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597282, + "record": { + "seq": 25, + "origin": "external", + "recorded_at": 1789709597282, + "body": { + "event": "step.progress.recorded", + "firing": 2, + "ev": { + "log": { + "stream": "stdout", + "line": "A haiku, added." + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 45, + "kind": "petri", + "id": "execution 0/26/0", + "recorded_at": 1789709597282, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 26, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597282, + "record": { + "seq": 26, + "origin": "external", + "recorded_at": 1789709597282, + "body": { + "event": "step.progress.recorded", + "firing": 2, + "ev": { + "custom": { + "kind": "pebble", + "firing": 2, + "attempt": 1, + "scope": 0, + "node": "greet", + "event": { + "seq": 10, + "stream_id": "ses_951f513e-ed1c-4c82-ad49-be68d093de09", + "event": "SessionEnded", + "timestamp": "2026-09-18T05:33:17.282Z", + "session_id": "ses_951f513e-ed1c-4c82-ad49-be68d093de09" + } + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 46, + "kind": "petri", + "id": "execution 0/27/0", + "recorded_at": 1789709597389, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 27, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597389, + "record": { + "seq": 27, + "origin": "external", + "recorded_at": 1789709597389, + "body": { + "event": "step.progress.recorded", + "firing": 2, + "ev": { + "custom": { + "$note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 2, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "577da173f630270470d5b7e484b0b4838f6e2506", + "reused": false + } + } + } + } + } + }, + "derived": { + "parsed": { + "kind": "note", + "note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 2, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "577da173f630270470d5b7e484b0b4838f6e2506", + "reused": false + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 47, + "kind": "petri", + "id": "execution 0/28/0", + "recorded_at": 1789709597390, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 28, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597390, + "record": { + "seq": 28, + "origin": "external", + "recorded_at": 1789709597390, + "body": { + "event": "step.finished", + "firing": 2, + "attempt": 1, + "outcome": { + "status": "success", + "output": { + "text": "A haiku, added.", + "turns": 1, + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 268, + "custom": { + "pebble.compaction_usage": { + "tokens": { + "input": 0, + "output": 0, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + } + }, + "pebble.compactions": 0, + "pebble.inference_ms": 0, + "pebble.prompts": 1, + "pebble.subagents": { + "spawned": 0, + "turns_started": 0, + "completed": 0, + "failed": 0, + "closed": 0, + "usage": { + "tokens": { + "input": 0, + "output": 0, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + } + }, + "sessions": {} + }, + "pebble.tool_ms": 0, + "pebble.usage": { + "tokens": { + "input": 1, + "output": 5, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + }, + "cost": { + "usd_micros": 77, + "source": "catalog" + } + } + } + }, + "context_updates": { + "failure_class": "", + "last_response": "A haiku, added.", + "last_stage": "greet", + "response.greet": "A haiku, added." + } + } + } + }, + "derived": { + "final": true, + "exhausted": false + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 48, + "kind": "petri", + "id": "execution 0/28/1", + "recorded_at": 1789709597390, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 28, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597390, + "derived": { + "event": "visit.completed", + "outcome": { + "status": "success", + "output": { + "text": "A haiku, added.", + "turns": 1, + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 268, + "custom": { + "pebble.compaction_usage": { + "tokens": { + "input": 0, + "output": 0, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + } + }, + "pebble.compactions": 0, + "pebble.inference_ms": 0, + "pebble.prompts": 1, + "pebble.subagents": { + "spawned": 0, + "turns_started": 0, + "completed": 0, + "failed": 0, + "closed": 0, + "usage": { + "tokens": { + "input": 0, + "output": 0, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + } + }, + "sessions": {} + }, + "pebble.tool_ms": 0, + "pebble.usage": { + "tokens": { + "input": 1, + "output": 5, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + }, + "cost": { + "usd_micros": 77, + "source": "catalog" + } + } + } + }, + "context_updates": { + "failure_class": "", + "last_response": "A haiku, added.", + "last_stage": "greet", + "response.greet": "A haiku, added." + } + }, + "executed": true, + "attempts": 1 + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 49, + "kind": "petri", + "id": "execution 0/29/0", + "recorded_at": 1789709597390, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 29, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597390, + "record": { + "seq": 29, + "origin": "external", + "recorded_at": 1789709597390, + "body": { + "event": "routing.resolved", + "decision_id": { + "route": { + "firing": 2, + "attempt": 1 + } + }, + "groups": [ + { + "group": 0, + "draw": null, + "trace": [], + "decision": { + "emit": 1 + } + } + ] + } + }, + "derived": { + "groups": [ + { + "group": 0, + "target": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "Exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 6, + "column": 5 + } + } + } + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 50, + "kind": "petri", + "id": "execution 0/29/1", + "recorded_at": 1789709597390, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 29, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "Exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 6, + "column": 5 + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597390, + "derived": { + "event": "visit.started", + "inputs": [ + { + "edge": 1, + "generation": 0, + "payload": { + "text": "A haiku, added.", + "turns": 1, + "outcome": "succeeded", + "failure_class": "" + }, + "from": 2 + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 51, + "kind": "petri", + "id": "execution 0/29/2", + "recorded_at": 1789709597390, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 29, + "index": 2 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "Exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 6, + "column": 5 + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597390, + "derived": { + "event": "wait.state.changed", + "state": "awaiting_admission" + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 52, + "kind": "petri", + "id": "execution 0/30/0", + "recorded_at": 1789709597390, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 30, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597390, + "record": { + "seq": 30, + "origin": "core", + "recorded_at": 1789709597390, + "body": { + "event": "route.applied", + "kind": "edge", + "firing": 2, + "group": 0, + "edge": 1 + } + }, + "derived": { + "target": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "Exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 6, + "column": 5 + } + } + }, + "transition": "Continue", + "back": false + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 53, + "kind": "petri", + "id": "execution 0/31/0", + "recorded_at": 1789709597390, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 31, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "greet", + "kind": "attractor/agent", + "meta": { + "label": "Greet", + "shape": "box", + "kind": "agent", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "1": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597390, + "record": { + "seq": 31, + "origin": "core", + "recorded_at": 1789709597390, + "body": { + "event": "token.emitted", + "edge": 1, + "generation": 0, + "payload": { + "text": "A haiku, added.", + "turns": 1, + "outcome": "succeeded", + "failure_class": "" + }, + "from": 2 + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 54, + "kind": "petri", + "id": "execution 0/32/0", + "recorded_at": 1789709597390, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 32, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "Exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 6, + "column": 5 + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597390, + "record": { + "seq": 32, + "origin": "external", + "recorded_at": 1789709597390, + "body": { + "event": "admission.decided", + "decision_id": { + "attempt_start": { + "firing": 3, + "attempt": 1 + } + }, + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 55, + "kind": "petri", + "id": "execution 0/33/0", + "recorded_at": 1789709597390, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 33, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "Exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 6, + "column": 5 + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597390, + "record": { + "seq": 33, + "origin": "external", + "recorded_at": 1789709597390, + "body": { + "event": "step.started", + "firing": 3, + "attempt": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 56, + "kind": "petri", + "id": "execution 0/33/1", + "recorded_at": 1789709597390, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 33, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "Exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 6, + "column": 5 + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597390, + "derived": { + "event": "wait.state.changed", + "state": "running" + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 57, + "kind": "platform", + "id": "8", + "recorded_at": 1789709597390, + "item": { + "seq": 8, + "recorded_at": 1789709597390, + "record": { + "kind": "checkpoint", + "execution": 0, + "firing": 2, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "577da173f630270470d5b7e484b0b4838f6e2506", + "operation": { + "execution": 0, + "decision": { + "attempt_start": { + "firing": 2, + "attempt": 1 + } + }, + "effect": "checkpoint" + } + }, + "position": { + "execution": 0, + "firing": 2 + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 58, + "kind": "petri", + "id": "execution 0/34/0", + "recorded_at": 1789709597492, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 34, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "Exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 6, + "column": 5 + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597492, + "record": { + "seq": 34, + "origin": "external", + "recorded_at": 1789709597492, + "body": { + "event": "step.progress.recorded", + "firing": 3, + "ev": { + "custom": { + "$note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 3, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "fc290eb55d3149ca2be57b3e39ee609904a5663d", + "reused": false + } + } + } + } + } + }, + "derived": { + "parsed": { + "kind": "note", + "note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 3, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "fc290eb55d3149ca2be57b3e39ee609904a5663d", + "reused": false + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 59, + "kind": "petri", + "id": "execution 0/35/0", + "recorded_at": 1789709597492, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 35, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "Exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 6, + "column": 5 + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597492, + "record": { + "seq": 35, + "origin": "external", + "recorded_at": 1789709597492, + "body": { + "event": "step.finished", + "firing": 3, + "attempt": 1, + "outcome": { + "status": "success", + "output": { + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 0 + }, + "context_updates": { + "failure_class": "" + } + } + } + }, + "derived": { + "final": true, + "exhausted": false + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 60, + "kind": "petri", + "id": "execution 0/35/1", + "recorded_at": 1789709597492, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 35, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "Exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 6, + "column": 5 + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597492, + "derived": { + "event": "visit.completed", + "outcome": { + "status": "success", + "output": { + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 0 + }, + "context_updates": { + "failure_class": "" + } + }, + "executed": true, + "attempts": 1 + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 61, + "kind": "platform", + "id": "9", + "recorded_at": 1789709597492, + "item": { + "seq": 9, + "recorded_at": 1789709597492, + "record": { + "kind": "checkpoint", + "execution": 0, + "firing": 3, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "fc290eb55d3149ca2be57b3e39ee609904a5663d", + "operation": { + "execution": 0, + "decision": { + "attempt_start": { + "firing": 3, + "attempt": 1 + } + }, + "effect": "checkpoint" + } + }, + "position": { + "execution": 0, + "firing": 3 + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 62, + "kind": "petri", + "id": "coordinator/4/0", + "recorded_at": 1789709597493, + "item": { + "id": { + "log": "coordinator", + "seq": 4, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709597493, + "record": { + "seq": 4, + "origin": "external", + "recorded_at": 1789709597493, + "body": { + "event": "execution.finished", + "execution": 0, + "exit": { + "terminal": { + "status": "success" + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 63, + "kind": "petri", + "id": "coordinator/5/0", + "recorded_at": 1789709597493, + "item": { + "id": { + "log": "coordinator", + "seq": 5, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709597493, + "record": { + "seq": 5, + "origin": "external", + "recorded_at": 1789709597493, + "body": { + "event": "invocation.finished", + "invocation": 0, + "result": { + "status": "success", + "failure": null, + "final_execution": 0, + "output": null, + "context": { + "failure_class": "", + "internal.run_id": "petri", + "last_response": "A haiku, added.", + "last_stage": "greet", + "response.greet": "A haiku, added." + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 64, + "kind": "petri", + "id": "execution 0/36/0", + "recorded_at": 1789709597493, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 36, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "Exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 6, + "column": 5 + } + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597493, + "record": { + "seq": 36, + "origin": "external", + "recorded_at": 1789709597493, + "body": { + "event": "routing.resolved", + "decision_id": { + "route": { + "firing": 3, + "attempt": 1 + } + }, + "groups": [] + } + }, + "derived": { + "groups": [] + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 65, + "kind": "petri", + "id": "coordinator/6/0", + "recorded_at": 1789709597534, + "item": { + "id": { + "log": "coordinator", + "seq": 6, + "index": 0 + }, + "origin": "external", + "context": {}, + "recorded_at": 1789709597534, + "record": { + "seq": 6, + "origin": "external", + "recorded_at": 1789709597534, + "body": { + "event": "run.finished", + "status": "success" + } + } + } + }, + { + "run_id": "01M2SG2E0BHHDQEF0ZKBQXR921", + "stream_seq": 66, + "kind": "platform", + "id": "10", + "recorded_at": 1789709597538, + "item": { + "seq": 10, + "recorded_at": 1789709597538, + "record": { + "kind": "run.lifecycle", + "transition": "succeeded", + "status": { + "kind": "succeeded", + "reason": "completed" + } + } + } + } + ] +} \ No newline at end of file diff --git a/apps/fabro-web/app/test-fixtures/petri/parallel.json b/apps/fabro-web/app/test-fixtures/petri/parallel.json new file mode 100644 index 000000000..1a7ecb031 --- /dev/null +++ b/apps/fabro-web/app/test-fixtures/petri/parallel.json @@ -0,0 +1,9945 @@ +{ + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "projection": { + "title": "Run two branches", + "spec": { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "settings": { + "project": { + "name": null, + "description": null, + "metadata": {} + }, + "workflow": { + "name": null, + "description": null, + "graph": "workflow.fabro", + "metadata": {} + }, + "run": { + "goal": null, + "working_dir": null, + "metadata": {}, + "inputs": {}, + "model": { + "provider": null, + "name": null, + "fallbacks": {}, + "controls": { + "reasoning_effort": null, + "speed": null + } + }, + "git": { + "author": null + }, + "prepare": { + "steps": [], + "timeout_ms": 300000 + }, + "execution": { + "mode": "normal", + "approval": "prompt" + }, + "checkpoint": { + "exclude_globs": [], + "skip_git_hooks": false, + "commit_timeout_ms": 30000 + }, + "clone": { + "enabled": true, + "depth": 100 + }, + "run_branch": { + "enabled": true, + "push": true + }, + "environment": { + "id": "local", + "provider": "local", + "image": { + "docker": null, + "dockerfile": null + }, + "resources": { + "cpu": null, + "memory": null, + "disk": null + }, + "network": { + "mode": "allow_all", + "allow": [] + }, + "lifecycle": { + "preserve": false, + "stop_on_terminal": true, + "auto_stop": null + }, + "labels": {}, + "env": {} + }, + "notifications": {}, + "interviews": { + "provider": null, + "slack": null + }, + "agent": { + "fabro_tools": false, + "mcps": {} + }, + "hooks": [], + "scm": { + "provider": null, + "owner": null, + "repository": null, + "github": null + }, + "pull_request": null, + "artifacts": { + "include": [] + }, + "integrations": { + "github": { + "permissions": {} + } + } + } + }, + "graph": { + "name": "Parallel", + "nodes": { + "exit": { + "id": "exit", + "attrs": { + "shape": { + "String": "Msquare" + } + } + }, + "start": { + "id": "start", + "attrs": { + "shape": { + "String": "Mdiamond" + } + } + }, + "fork": { + "id": "fork", + "attrs": { + "shape": { + "String": "component" + } + } + }, + "a": { + "id": "a", + "attrs": { + "shape": { + "String": "parallelogram" + }, + "script": { + "String": "touch /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpFCclqg/a.started; while [ ! -f /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpFCclqg/go ]; do sleep 0.05; done; echo a" + } + } + }, + "merge": { + "id": "merge", + "attrs": { + "shape": { + "String": "tripleoctagon" + } + } + }, + "b": { + "id": "b", + "attrs": { + "shape": { + "String": "parallelogram" + }, + "script": { + "String": "touch /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpFCclqg/b.started; while [ ! -f /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpFCclqg/go ]; do sleep 0.05; done; echo b" + } + } + } + }, + "edges": [ + { + "from": "start", + "to": "fork", + "attrs": {} + }, + { + "from": "fork", + "to": "a", + "attrs": {} + }, + { + "from": "fork", + "to": "b", + "attrs": {} + }, + { + "from": "a", + "to": "merge", + "attrs": {} + }, + { + "from": "b", + "to": "merge", + "attrs": {} + }, + { + "from": "merge", + "to": "exit", + "attrs": {} + } + ], + "attrs": { + "goal": { + "String": "Run two branches" + } + } + }, + "graph_source": "digraph Parallel {\n graph [goal=\"Run two branches\"]\n start [shape=Mdiamond]\n exit [shape=Msquare]\n fork [shape=component]\n a [shape=parallelogram, script=\"touch /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpFCclqg/a.started; while [ ! -f /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpFCclqg/go ]; do sleep 0.05; done; echo a\"]\n b [shape=parallelogram, script=\"touch /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpFCclqg/b.started; while [ ! -f /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpFCclqg/go ]; do sleep 0.05; done; echo b\"]\n merge [shape=tripleoctagon]\n start -> fork\n fork -> a\n fork -> b\n a -> merge\n b -> merge\n merge -> exit\n}", + "workflow_slug": "workflow", + "workflow_version_id": "03d033a0c4364ef6084e2d3020cce8abf0dc7c02e6ecefacf320857ca7eb70df", + "target": { + "kind": "folder", + "path": "/private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpmiL7PK" + }, + "source_directory": "/private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpmiL7PK", + "provenance": { + "server": { + "version": "0.357.0-nightly.0" + }, + "subject": { + "kind": "user", + "identity": { + "issuer": "fabro:dev", + "subject": "dev" + }, + "login": "dev", + "auth_method": "dev_token" + } + }, + "spec_blob": "3006b809a73d1f6e228bb091c016b75a7a3121aaef0eb00d33baf1ae8ee105dc", + "engine": { + "kind": "petri", + "graph": { + "blob": "7401352516fb5ee5ebeedf2057e6235cee8d7ffedbbc2a116bb30ea24f7b2a61", + "digest": "7401352516fb5ee5ebeedf2057e6235cee8d7ffedbbc2a116bb30ea24f7b2a61" + }, + "children": [ + { + "blob": "2b8e4e9c97881bdf04d48f57815fc6633959c953fbc786c7f0ce76488d718542", + "digest": "2b8e4e9c97881bdf04d48f57815fc6633959c953fbc786c7f0ce76488d718542" + }, + { + "blob": "940ba00308da213651f5dfcfbe78408688ae4d5deb9f96a4356536e2ddf51883", + "digest": "940ba00308da213651f5dfcfbe78408688ae4d5deb9f96a4356536e2ddf51883" + } + ] + } + }, + "web_url": "http://localhost:3000/runs/01M2SG2E0BQSRJ1G31WRPX542Y", + "start": { + "start_time": "2026-09-18T05:33:16.740Z" + }, + "status": { + "kind": "succeeded", + "reason": "completed" + }, + "status_updated_at": "2026-09-18T05:33:17.838Z", + "last_event_at": "2026-09-18T05:33:17.843Z", + "pending_control": null, + "checkpoints": [ + { + "seq": 33, + "checkpoint": { + "timestamp": "2026-09-18T05:33:17.013Z", + "current_node": "start", + "completed_nodes": [], + "node_retries": {}, + "context_values": {}, + "git_commit_sha": "ba079ac5c25e2c349df023cc66ec5875d11e2b18" + }, + "diff": {} + }, + { + "seq": 47, + "checkpoint": { + "timestamp": "2026-09-18T05:33:17.117Z", + "current_node": "fork", + "completed_nodes": [], + "node_retries": {}, + "context_values": {}, + "git_commit_sha": "beeaef66193b2dc4b53a704b6293e68e0db5aa27" + }, + "diff": {} + }, + { + "seq": 83, + "checkpoint": { + "timestamp": "2026-09-18T05:33:17.317Z", + "current_node": "a", + "completed_nodes": [], + "node_retries": {}, + "context_values": {}, + "git_commit_sha": "abda2b70b2a2627e829428c1d0aa50169b7b17f8" + }, + "diff": {} + }, + { + "seq": 91, + "checkpoint": { + "timestamp": "2026-09-18T05:33:17.433Z", + "current_node": "b", + "completed_nodes": [], + "node_retries": {}, + "context_values": {}, + "git_commit_sha": "572c40a80a2b6456e7dd68d63a0fce6416805fab" + }, + "diff": {} + }, + { + "seq": 101, + "checkpoint": { + "timestamp": "2026-09-18T05:33:17.525Z", + "current_node": "a", + "completed_nodes": [], + "node_retries": {}, + "context_values": {}, + "git_commit_sha": "d19179e7075e6d260660832c2f26e3a31efecd20" + }, + "diff": {} + }, + { + "seq": 117, + "checkpoint": { + "timestamp": "2026-09-18T05:33:17.618Z", + "current_node": "b", + "completed_nodes": [], + "node_retries": {}, + "context_values": {}, + "git_commit_sha": "9606890ac1f59546498ea7167131f9e542edd05f" + }, + "diff": {} + }, + { + "seq": 129, + "checkpoint": { + "timestamp": "2026-09-18T05:33:17.709Z", + "current_node": "merge", + "completed_nodes": [], + "node_retries": {}, + "context_values": {}, + "git_commit_sha": "f99391338a6f1b460ca44095fed82a92dfe7e0a0" + }, + "diff": {} + }, + { + "seq": 136, + "checkpoint": { + "timestamp": "2026-09-18T05:33:17.798Z", + "current_node": "exit", + "completed_nodes": [], + "node_retries": {}, + "context_values": {}, + "git_commit_sha": "a1f942a57396c23c378fde187c43b15adbb7e648" + }, + "diff": {} + } + ], + "conclusion": { + "timestamp": "2026-09-18T05:33:17.838Z", + "status": "succeeded", + "timing": { + "wall_time_ms": 1098, + "inference_time_ms": 0, + "tool_time_ms": 194, + "active_time_ms": 194 + }, + "final_git_commit_sha": "a1f942a57396c23c378fde187c43b15adbb7e648", + "total_retries": 0, + "diff": {} + }, + "sandbox": { + "kind": "planned", + "plan": { + "provider": "local" + } + }, + "pull_request": null, + "superseded_by": null, + "pending_interviews": {}, + "stages": { + "a@1": { + "first_event_seq": 440, + "prompt": null, + "response": null, + "completion": { + "outcome": "succeeded", + "notes": null, + "failure_reason": null, + "timestamp": "2026-09-18T05:33:17.317Z" + }, + "provider_used": null, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "parallel_branch_id": "fork@1:0", + "output": "a\n", + "output_bytes": 2, + "live_streaming": false, + "termination": "exited", + "started_at": "2026-09-18T05:33:17.122Z", + "handler": "command", + "graph_visit": 1, + "timing": { + "wall_time_ms": 96, + "inference_time_ms": 0, + "tool_time_ms": 96, + "active_time_ms": 96 + }, + "usage": { + "tokens": { + "input": 0, + "output": 0, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + } + }, + "state": "succeeded" + }, + "start@1": { + "first_event_seq": 61, + "prompt": null, + "response": null, + "completion": { + "outcome": "succeeded", + "notes": null, + "failure_reason": null, + "timestamp": "2026-09-18T05:33:17.013Z" + }, + "provider_used": null, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "output": "checkout: /private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpmiL7PK is not a Git repository; the workspace starts empty\n", + "output_bytes": 130, + "live_streaming": false, + "termination": "exited", + "started_at": "2026-09-18T05:33:16.743Z", + "handler": "start", + "graph_visit": 1, + "timing": { + "wall_time_ms": 10, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, + "usage": { + "tokens": { + "input": 0, + "output": 0, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + } + }, + "state": "succeeded" + }, + "exit@1": { + "first_event_seq": 1027, + "prompt": null, + "response": null, + "completion": { + "outcome": "succeeded", + "notes": null, + "failure_reason": null, + "timestamp": "2026-09-18T05:33:17.798Z" + }, + "provider_used": null, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "output": null, + "live_streaming": false, + "termination": "exited", + "started_at": "2026-09-18T05:33:17.709Z", + "handler": "exit", + "graph_visit": 1, + "timing": { + "wall_time_ms": 0, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, + "usage": { + "tokens": { + "input": 0, + "output": 0, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + } + }, + "state": "succeeded" + }, + "fork@1": { + "first_event_seq": 331, + "prompt": null, + "response": null, + "completion": { + "outcome": "succeeded", + "notes": null, + "failure_reason": null, + "timestamp": "2026-09-18T05:33:17.117Z" + }, + "provider_used": null, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": [ + { + "id": "a", + "index": 0, + "status": "succeeded", + "context_updates": {} + }, + { + "id": "b", + "index": 1, + "status": "succeeded", + "context_updates": {} + } + ], + "output": null, + "live_streaming": false, + "termination": "exited", + "started_at": "2026-09-18T05:33:17.013Z", + "handler": "parallel", + "graph_visit": 1, + "timing": { + "wall_time_ms": 0, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, + "usage": { + "tokens": { + "input": 0, + "output": 0, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + } + }, + "state": "succeeded" + }, + "b@1": { + "first_event_seq": 438, + "prompt": null, + "response": null, + "completion": { + "outcome": "succeeded", + "notes": null, + "failure_reason": null, + "timestamp": "2026-09-18T05:33:17.433Z" + }, + "provider_used": null, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "parallel_branch_id": "fork@1:1", + "output": "b\n", + "output_bytes": 2, + "live_streaming": false, + "termination": "exited", + "started_at": "2026-09-18T05:33:17.120Z", + "handler": "command", + "graph_visit": 1, + "timing": { + "wall_time_ms": 98, + "inference_time_ms": 0, + "tool_time_ms": 98, + "active_time_ms": 98 + }, + "usage": { + "tokens": { + "input": 0, + "output": 0, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + } + }, + "state": "succeeded" + }, + "merge@1": { + "first_event_seq": 936, + "prompt": null, + "response": null, + "completion": { + "outcome": "succeeded", + "notes": null, + "failure_reason": null, + "timestamp": "2026-09-18T05:33:17.709Z" + }, + "provider_used": null, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "output": null, + "live_streaming": false, + "termination": "exited", + "started_at": "2026-09-18T05:33:17.618Z", + "handler": "parallel.fan_in", + "graph_visit": 1, + "timing": { + "wall_time_ms": 0, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, + "usage": { + "tokens": { + "input": 0, + "output": 0, + "reasoning": 0, + "cache_read": 0, + "cache_write": 0 + } + }, + "state": "succeeded" + } + } + }, + "stream": [ + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 1, + "kind": "platform", + "id": "1", + "recorded_at": 1789709596683, + "item": { + "seq": 1, + "recorded_at": 1789709596683, + "record": { + "kind": "run.created", + "spec": { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "settings": { + "project": { + "name": null, + "description": null, + "metadata": {} + }, + "workflow": { + "name": null, + "description": null, + "graph": "workflow.fabro", + "metadata": {} + }, + "run": { + "goal": null, + "working_dir": null, + "metadata": {}, + "inputs": {}, + "model": { + "provider": null, + "name": null, + "fallbacks": {}, + "controls": { + "reasoning_effort": null, + "speed": null + } + }, + "git": { + "author": null + }, + "prepare": { + "steps": [], + "timeout_ms": 300000 + }, + "execution": { + "mode": "normal", + "approval": "prompt" + }, + "checkpoint": { + "exclude_globs": [], + "skip_git_hooks": false, + "commit_timeout_ms": 30000 + }, + "clone": { + "enabled": true, + "depth": 100 + }, + "run_branch": { + "enabled": true, + "push": true + }, + "environment": { + "id": "local", + "provider": "local", + "image": { + "docker": null, + "dockerfile": null + }, + "resources": { + "cpu": null, + "memory": null, + "disk": null + }, + "network": { + "mode": "allow_all", + "allow": [] + }, + "lifecycle": { + "preserve": false, + "stop_on_terminal": true, + "auto_stop": null + }, + "labels": {}, + "env": {} + }, + "notifications": {}, + "interviews": { + "provider": null, + "slack": null + }, + "agent": { + "fabro_tools": false, + "mcps": {} + }, + "hooks": [], + "scm": { + "provider": null, + "owner": null, + "repository": null, + "github": null + }, + "pull_request": null, + "artifacts": { + "include": [] + }, + "integrations": { + "github": { + "permissions": {} + } + } + } + }, + "graph": { + "name": "Parallel", + "nodes": { + "fork": { + "id": "fork", + "attrs": { + "shape": { + "String": "component" + } + } + }, + "merge": { + "id": "merge", + "attrs": { + "shape": { + "String": "tripleoctagon" + } + } + }, + "start": { + "id": "start", + "attrs": { + "shape": { + "String": "Mdiamond" + } + } + }, + "exit": { + "id": "exit", + "attrs": { + "shape": { + "String": "Msquare" + } + } + }, + "a": { + "id": "a", + "attrs": { + "shape": { + "String": "parallelogram" + }, + "script": { + "String": "touch /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpFCclqg/a.started; while [ ! -f /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpFCclqg/go ]; do sleep 0.05; done; echo a" + } + } + }, + "b": { + "id": "b", + "attrs": { + "script": { + "String": "touch /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpFCclqg/b.started; while [ ! -f /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpFCclqg/go ]; do sleep 0.05; done; echo b" + }, + "shape": { + "String": "parallelogram" + } + } + } + }, + "edges": [ + { + "from": "start", + "to": "fork", + "attrs": {} + }, + { + "from": "fork", + "to": "a", + "attrs": {} + }, + { + "from": "fork", + "to": "b", + "attrs": {} + }, + { + "from": "a", + "to": "merge", + "attrs": {} + }, + { + "from": "b", + "to": "merge", + "attrs": {} + }, + { + "from": "merge", + "to": "exit", + "attrs": {} + } + ], + "attrs": { + "goal": { + "String": "Run two branches" + } + } + }, + "graph_source": "digraph Parallel {\n graph [goal=\"Run two branches\"]\n start [shape=Mdiamond]\n exit [shape=Msquare]\n fork [shape=component]\n a [shape=parallelogram, script=\"touch /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpFCclqg/a.started; while [ ! -f /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpFCclqg/go ]; do sleep 0.05; done; echo a\"]\n b [shape=parallelogram, script=\"touch /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpFCclqg/b.started; while [ ! -f /var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpFCclqg/go ]; do sleep 0.05; done; echo b\"]\n merge [shape=tripleoctagon]\n start -> fork\n fork -> a\n fork -> b\n a -> merge\n b -> merge\n merge -> exit\n}", + "workflow_slug": "workflow", + "workflow_version_id": "03d033a0c4364ef6084e2d3020cce8abf0dc7c02e6ecefacf320857ca7eb70df", + "target": { + "kind": "folder", + "path": "/private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpmiL7PK" + }, + "source_directory": "/private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpmiL7PK", + "provenance": { + "server": { + "version": "0.357.0-nightly.0" + }, + "subject": { + "kind": "user", + "identity": { + "issuer": "fabro:dev", + "subject": "dev" + }, + "login": "dev", + "auth_method": "dev_token" + } + }, + "spec_blob": "3006b809a73d1f6e228bb091c016b75a7a3121aaef0eb00d33baf1ae8ee105dc", + "engine": { + "kind": "petri", + "graph": { + "blob": "7401352516fb5ee5ebeedf2057e6235cee8d7ffedbbc2a116bb30ea24f7b2a61", + "digest": "7401352516fb5ee5ebeedf2057e6235cee8d7ffedbbc2a116bb30ea24f7b2a61" + }, + "children": [ + { + "blob": "2b8e4e9c97881bdf04d48f57815fc6633959c953fbc786c7f0ce76488d718542", + "digest": "2b8e4e9c97881bdf04d48f57815fc6633959c953fbc786c7f0ce76488d718542" + }, + { + "blob": "940ba00308da213651f5dfcfbe78408688ae4d5deb9f96a4356536e2ddf51883", + "digest": "940ba00308da213651f5dfcfbe78408688ae4d5deb9f96a4356536e2ddf51883" + } + ] + } + }, + "title": "Run two branches", + "web_url": "http://localhost:3000/runs/01M2SG2E0BQSRJ1G31WRPX542Y" + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 2, + "kind": "platform", + "id": "2", + "recorded_at": 1789709596730, + "item": { + "seq": 2, + "recorded_at": 1789709596730, + "record": { + "kind": "run.lifecycle", + "transition": "submitted", + "status": { + "kind": "submitted" + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 3, + "kind": "platform", + "id": "3", + "recorded_at": 1789709596735, + "item": { + "seq": 3, + "recorded_at": 1789709596735, + "record": { + "kind": "run.lifecycle", + "transition": "start_requested", + "source": "start" + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 4, + "kind": "platform", + "id": "4", + "recorded_at": 1789709596736, + "item": { + "seq": 4, + "recorded_at": 1789709596736, + "record": { + "kind": "run.lifecycle", + "transition": "runnable", + "status": { + "kind": "runnable" + }, + "source": "start_requested" + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 5, + "kind": "platform", + "id": "5", + "recorded_at": 1789709596737, + "item": { + "seq": 5, + "recorded_at": 1789709596737, + "record": { + "kind": "run.lifecycle", + "transition": "starting", + "status": { + "kind": "starting" + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 6, + "kind": "platform", + "id": "6", + "recorded_at": 1789709596738, + "item": { + "seq": 6, + "recorded_at": 1789709596738, + "record": { + "kind": "run.lifecycle", + "transition": "running", + "status": { + "kind": "running" + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 7, + "kind": "petri", + "id": "coordinator/0/0", + "recorded_at": 1789709596740, + "item": { + "id": { + "log": "coordinator", + "seq": 0, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0 + }, + "recorded_at": 1789709596740, + "record": { + "seq": 0, + "origin": "external", + "recorded_at": 1789709596740, + "body": { + "event": "run.started", + "format_version": 5, + "key": "01M2SG2E0BQSRJ1G31WRPX542Y", + "root": 0, + "middleware_chain": [ + "circuit-breaker" + ] + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 8, + "kind": "petri", + "id": "coordinator/1/0", + "recorded_at": 1789709596741, + "item": { + "id": { + "log": "coordinator", + "seq": 1, + "index": 0 + }, + "origin": "external", + "context": {}, + "recorded_at": 1789709596741, + "record": { + "seq": 1, + "origin": "external", + "recorded_at": 1789709596741, + "body": { + "event": "graph.registered", + "digest": "7401352516fb5ee5ebeedf2057e6235cee8d7ffedbbc2a116bb30ea24f7b2a61" + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 9, + "kind": "petri", + "id": "coordinator/2/0", + "recorded_at": 1789709596741, + "item": { + "id": { + "log": "coordinator", + "seq": 2, + "index": 0 + }, + "origin": "external", + "context": {}, + "recorded_at": 1789709596741, + "record": { + "seq": 2, + "origin": "external", + "recorded_at": 1789709596741, + "body": { + "event": "graph.registered", + "digest": "2b8e4e9c97881bdf04d48f57815fc6633959c953fbc786c7f0ce76488d718542" + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 10, + "kind": "petri", + "id": "coordinator/3/0", + "recorded_at": 1789709596742, + "item": { + "id": { + "log": "coordinator", + "seq": 3, + "index": 0 + }, + "origin": "external", + "context": {}, + "recorded_at": 1789709596742, + "record": { + "seq": 3, + "origin": "external", + "recorded_at": 1789709596742, + "body": { + "event": "graph.registered", + "digest": "940ba00308da213651f5dfcfbe78408688ae4d5deb9f96a4356536e2ddf51883" + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 11, + "kind": "petri", + "id": "coordinator/4/0", + "recorded_at": 1789709596742, + "item": { + "id": { + "log": "coordinator", + "seq": 4, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0 + }, + "recorded_at": 1789709596742, + "record": { + "seq": 4, + "origin": "external", + "recorded_at": 1789709596742, + "body": { + "event": "invocation.declared", + "invocation": 0, + "call": null, + "graph": "7401352516fb5ee5ebeedf2057e6235cee8d7ffedbbc2a116bb30ea24f7b2a61", + "context": {}, + "secret_bindings": "none", + "sandbox": "isolated" + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 12, + "kind": "petri", + "id": "coordinator/5/0", + "recorded_at": 1789709596742, + "item": { + "id": { + "log": "coordinator", + "seq": 5, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709596742, + "record": { + "seq": 5, + "origin": "external", + "recorded_at": 1789709596742, + "body": { + "event": "execution.declared", + "execution": 0, + "invocation": 0, + "predecessor": null, + "start": { + "entry": "graph_entries", + "context": {}, + "prior_firings": {}, + "execution_index": 0, + "max_executions": 32 + }, + "middleware_state": { + "circuit-breaker": [ + 1, + { + "loop_signatures": {}, + "restart_signatures": {}, + "pending": {} + } + ] + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 13, + "kind": "petri", + "id": "execution 0/0/0", + "recorded_at": 1789709596743, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 0, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709596743, + "record": { + "seq": 0, + "origin": "external", + "recorded_at": 1789709596743, + "body": { + "event": "execution.started", + "entry": "graph_entries", + "context": {}, + "prior_firings": {}, + "execution_index": 0, + "max_executions": 32 + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 14, + "kind": "petri", + "id": "execution 0/1/0", + "recorded_at": 1789709596743, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 1, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709596743, + "record": { + "seq": 1, + "origin": "external", + "recorded_at": 1789709596743, + "body": { + "event": "admission.decided", + "decision_id": "execution_start", + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 15, + "kind": "petri", + "id": "execution 0/1/1", + "recorded_at": 1789709596743, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 1, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "fork", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596743, + "derived": { + "event": "visit.started", + "inputs": [ + { + "edge": 8, + "generation": 0, + "payload": null, + "from": 0 + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 16, + "kind": "petri", + "id": "execution 0/1/2", + "recorded_at": 1789709596743, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 1, + "index": 2 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "fork", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596743, + "derived": { + "event": "wait.state.changed", + "state": "awaiting_admission" + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 17, + "kind": "petri", + "id": "execution 0/2/0", + "recorded_at": 1789709596743, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 2, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709596743, + "record": { + "seq": 2, + "origin": "core", + "recorded_at": 1789709596743, + "body": { + "event": "token.emitted", + "edge": 8, + "generation": 0, + "payload": null, + "from": 0 + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 18, + "kind": "petri", + "id": "execution 0/3/0", + "recorded_at": 1789709596744, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 3, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "fork", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596744, + "record": { + "seq": 3, + "origin": "external", + "recorded_at": 1789709596744, + "body": { + "event": "admission.decided", + "decision_id": { + "attempt_start": { + "firing": 1, + "attempt": 1 + } + }, + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 19, + "kind": "petri", + "id": "execution 0/4/0", + "recorded_at": 1789709596825, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 4, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "fork", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596825, + "record": { + "seq": 4, + "origin": "external", + "recorded_at": 1789709596825, + "body": { + "event": "step.started", + "firing": 1, + "attempt": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 20, + "kind": "petri", + "id": "execution 0/4/1", + "recorded_at": 1789709596825, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 4, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "fork", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596825, + "derived": { + "event": "wait.state.changed", + "state": "running" + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 21, + "kind": "petri", + "id": "execution 0/5/0", + "recorded_at": 1789709596835, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 5, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "fork", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709596835, + "record": { + "seq": 5, + "origin": "external", + "recorded_at": 1789709596835, + "body": { + "event": "step.progress.recorded", + "firing": 1, + "ev": { + "log": { + "stream": "stderr", + "line": "checkout: /private/var/folders/nm/bd2dwzy52t1ckn3tklsfmy1c0000gn/T/.tmpmiL7PK is not a Git repository; the workspace starts empty" + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 22, + "kind": "petri", + "id": "execution 0/6/0", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 6, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "fork", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "record": { + "seq": 6, + "origin": "external", + "recorded_at": 1789709597013, + "body": { + "event": "step.progress.recorded", + "firing": 1, + "ev": { + "custom": { + "$note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 1, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "ba079ac5c25e2c349df023cc66ec5875d11e2b18", + "reused": false + } + } + } + } + } + }, + "derived": { + "parsed": { + "kind": "note", + "note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 1, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "ba079ac5c25e2c349df023cc66ec5875d11e2b18", + "reused": false + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 23, + "kind": "petri", + "id": "execution 0/7/0", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 7, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "fork", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "record": { + "seq": 7, + "origin": "external", + "recorded_at": 1789709597013, + "body": { + "event": "step.finished", + "firing": 1, + "attempt": 1, + "outcome": { + "status": "success", + "output": { + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 10 + }, + "context_updates": { + "failure_class": "", + "internal.run_id": "petri" + } + } + } + }, + "derived": { + "final": true, + "exhausted": false + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 24, + "kind": "petri", + "id": "execution 0/7/1", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 7, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "fork", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "derived": { + "event": "visit.completed", + "outcome": { + "status": "success", + "output": { + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 10 + }, + "context_updates": { + "failure_class": "", + "internal.run_id": "petri" + } + }, + "executed": true, + "attempts": 1 + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 25, + "kind": "petri", + "id": "execution 0/8/0", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 8, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "fork", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "record": { + "seq": 8, + "origin": "external", + "recorded_at": 1789709597013, + "body": { + "event": "routing.resolved", + "decision_id": { + "route": { + "firing": 1, + "attempt": 1 + } + }, + "groups": [ + { + "group": 0, + "draw": null, + "trace": [], + "decision": { + "emit": 0 + } + } + ] + } + }, + "derived": { + "groups": [ + { + "group": 0, + "target": { + "id": 2, + "name": "fork", + "kind": "attractor/fork", + "meta": { + "label": "fork", + "shape": "component", + "kind": "parallel", + "classes": [], + "span": { + "line": 5, + "column": 5 + } + } + } + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 26, + "kind": "petri", + "id": "execution 0/8/1", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 8, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "fork", + "kind": "attractor/fork", + "meta": { + "label": "fork", + "shape": "component", + "kind": "parallel", + "classes": [], + "span": { + "line": 5, + "column": 5 + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "fork", + "branches": 2 + } + }, + "recorded_at": 1789709597013, + "derived": { + "event": "visit.started", + "inputs": [ + { + "edge": 0, + "generation": 0, + "payload": { + "outcome": "succeeded", + "failure_class": "" + }, + "from": 1 + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 27, + "kind": "petri", + "id": "execution 0/8/2", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 8, + "index": 2 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "fork", + "kind": "attractor/fork", + "meta": { + "label": "fork", + "shape": "component", + "kind": "parallel", + "classes": [], + "span": { + "line": 5, + "column": 5 + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "fork", + "branches": 2 + } + }, + "recorded_at": 1789709597013, + "derived": { + "event": "wait.state.changed", + "state": "awaiting_admission" + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 28, + "kind": "petri", + "id": "execution 0/9/0", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 9, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "fork", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "record": { + "seq": 9, + "origin": "core", + "recorded_at": 1789709597013, + "body": { + "event": "route.applied", + "kind": "edge", + "firing": 1, + "group": 0, + "edge": 0 + } + }, + "derived": { + "target": { + "id": 2, + "name": "fork", + "kind": "attractor/fork", + "meta": { + "label": "fork", + "shape": "component", + "kind": "parallel", + "classes": [], + "span": { + "line": 5, + "column": 5 + } + } + }, + "transition": "Continue", + "back": false + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 29, + "kind": "petri", + "id": "execution 0/10/0", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 10, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 0, + "name": "start", + "kind": "attractor/stage", + "meta": { + "label": "start", + "shape": "Mdiamond", + "kind": "start", + "classes": [], + "span": { + "line": 3, + "column": 5 + }, + "admission_hooks": "step", + "edges": { + "0": { + "to": "fork", + "label": null + } + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597013, + "record": { + "seq": 10, + "origin": "core", + "recorded_at": 1789709597013, + "body": { + "event": "token.emitted", + "edge": 0, + "generation": 0, + "payload": { + "outcome": "succeeded", + "failure_class": "" + }, + "from": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 30, + "kind": "petri", + "id": "execution 0/11/0", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 11, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "fork", + "kind": "attractor/fork", + "meta": { + "label": "fork", + "shape": "component", + "kind": "parallel", + "classes": [], + "span": { + "line": 5, + "column": 5 + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "fork", + "branches": 2 + } + }, + "recorded_at": 1789709597013, + "record": { + "seq": 11, + "origin": "external", + "recorded_at": 1789709597013, + "body": { + "event": "admission.decided", + "decision_id": { + "attempt_start": { + "firing": 2, + "attempt": 1 + } + }, + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 31, + "kind": "petri", + "id": "execution 0/12/0", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 12, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "fork", + "kind": "attractor/fork", + "meta": { + "label": "fork", + "shape": "component", + "kind": "parallel", + "classes": [], + "span": { + "line": 5, + "column": 5 + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "fork", + "branches": 2 + } + }, + "recorded_at": 1789709597013, + "record": { + "seq": 12, + "origin": "external", + "recorded_at": 1789709597013, + "body": { + "event": "step.started", + "firing": 2, + "attempt": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 32, + "kind": "petri", + "id": "execution 0/12/1", + "recorded_at": 1789709597013, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 12, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "fork", + "kind": "attractor/fork", + "meta": { + "label": "fork", + "shape": "component", + "kind": "parallel", + "classes": [], + "span": { + "line": 5, + "column": 5 + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "fork", + "branches": 2 + } + }, + "recorded_at": 1789709597013, + "derived": { + "event": "wait.state.changed", + "state": "running" + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 33, + "kind": "platform", + "id": "7", + "recorded_at": 1789709597013, + "item": { + "seq": 7, + "recorded_at": 1789709597013, + "record": { + "kind": "checkpoint", + "execution": 0, + "firing": 1, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "ba079ac5c25e2c349df023cc66ec5875d11e2b18", + "operation": { + "execution": 0, + "decision": { + "attempt_start": { + "firing": 1, + "attempt": 1 + } + }, + "effect": "checkpoint" + } + }, + "position": { + "execution": 0, + "firing": 1 + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 34, + "kind": "petri", + "id": "execution 0/13/0", + "recorded_at": 1789709597117, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 13, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "fork", + "kind": "attractor/fork", + "meta": { + "label": "fork", + "shape": "component", + "kind": "parallel", + "classes": [], + "span": { + "line": 5, + "column": 5 + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "fork", + "branches": 2 + } + }, + "recorded_at": 1789709597117, + "record": { + "seq": 13, + "origin": "external", + "recorded_at": 1789709597117, + "body": { + "event": "step.progress.recorded", + "firing": 2, + "ev": { + "custom": { + "$note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 2, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "beeaef66193b2dc4b53a704b6293e68e0db5aa27", + "reused": false + } + } + } + } + } + }, + "derived": { + "parsed": { + "kind": "note", + "note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 2, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "beeaef66193b2dc4b53a704b6293e68e0db5aa27", + "reused": false + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 35, + "kind": "petri", + "id": "execution 0/14/0", + "recorded_at": 1789709597117, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 14, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "fork", + "kind": "attractor/fork", + "meta": { + "label": "fork", + "shape": "component", + "kind": "parallel", + "classes": [], + "span": { + "line": 5, + "column": 5 + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "fork", + "branches": 2 + } + }, + "recorded_at": 1789709597117, + "record": { + "seq": 14, + "origin": "external", + "recorded_at": 1789709597117, + "body": { + "event": "step.finished", + "firing": 2, + "attempt": 1, + "outcome": { + "status": "success", + "output": { + "snapshot": { + "failure_class": "", + "internal.run_id": "petri" + }, + "nodes": null, + "occurrence": { + "fork": "fork", + "firing": 2 + } + }, + "metrics": { + "duration_ms": 0 + } + } + } + }, + "derived": { + "final": true, + "exhausted": false + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 36, + "kind": "petri", + "id": "execution 0/14/1", + "recorded_at": 1789709597117, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 14, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "fork", + "kind": "attractor/fork", + "meta": { + "label": "fork", + "shape": "component", + "kind": "parallel", + "classes": [], + "span": { + "line": 5, + "column": 5 + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "fork", + "branches": 2 + } + }, + "recorded_at": 1789709597117, + "derived": { + "event": "visit.completed", + "outcome": { + "status": "success", + "output": { + "snapshot": { + "failure_class": "", + "internal.run_id": "petri" + }, + "nodes": null, + "occurrence": { + "fork": "fork", + "firing": 2 + } + }, + "metrics": { + "duration_ms": 0 + } + }, + "executed": true, + "attempts": 1 + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 37, + "kind": "petri", + "id": "execution 0/15/0", + "recorded_at": 1789709597117, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 15, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "fork", + "kind": "attractor/fork", + "meta": { + "label": "fork", + "shape": "component", + "kind": "parallel", + "classes": [], + "span": { + "line": 5, + "column": 5 + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "fork", + "branches": 2 + } + }, + "recorded_at": 1789709597117, + "record": { + "seq": 15, + "origin": "external", + "recorded_at": 1789709597117, + "body": { + "event": "routing.resolved", + "decision_id": { + "route": { + "firing": 2, + "attempt": 1 + } + }, + "groups": [ + { + "group": 0, + "draw": null, + "trace": [], + "decision": { + "emit": 6 + } + }, + { + "group": 1, + "draw": null, + "trace": [], + "decision": { + "emit": 7 + } + } + ] + } + }, + "derived": { + "groups": [ + { + "group": 0, + "target": { + "id": 3, + "name": "a", + "kind": "attractor/branch", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "a", + "index": 0 + }, + "synthetic": true + } + } + }, + { + "group": 1, + "target": { + "id": 4, + "name": "b", + "kind": "attractor/branch", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "b", + "index": 1 + }, + "synthetic": true + } + } + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 38, + "kind": "petri", + "id": "execution 0/15/1", + "recorded_at": 1789709597117, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 15, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 3, + "name": "a", + "kind": "attractor/branch", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "a", + "index": 0 + }, + "synthetic": true + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597117, + "derived": { + "event": "visit.started", + "inputs": [ + { + "edge": 6, + "generation": 0, + "payload": { + "snapshot": { + "failure_class": "", + "internal.run_id": "petri" + }, + "nodes": null, + "occurrence": { + "fork": "fork", + "firing": 2 + } + }, + "from": 2 + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 39, + "kind": "petri", + "id": "execution 0/15/2", + "recorded_at": 1789709597117, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 15, + "index": 2 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 3, + "name": "a", + "kind": "attractor/branch", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "a", + "index": 0 + }, + "synthetic": true + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597117, + "derived": { + "event": "wait.state.changed", + "state": "awaiting_admission" + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 40, + "kind": "petri", + "id": "execution 0/15/3", + "recorded_at": 1789709597117, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 15, + "index": 3 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "b", + "kind": "attractor/branch", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "b", + "index": 1 + }, + "synthetic": true + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597117, + "derived": { + "event": "visit.started", + "inputs": [ + { + "edge": 7, + "generation": 0, + "payload": { + "snapshot": { + "failure_class": "", + "internal.run_id": "petri" + }, + "nodes": null, + "occurrence": { + "fork": "fork", + "firing": 2 + } + }, + "from": 2 + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 41, + "kind": "petri", + "id": "execution 0/15/4", + "recorded_at": 1789709597117, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 15, + "index": 4 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "b", + "kind": "attractor/branch", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "b", + "index": 1 + }, + "synthetic": true + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597117, + "derived": { + "event": "wait.state.changed", + "state": "awaiting_admission" + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 42, + "kind": "petri", + "id": "execution 0/16/0", + "recorded_at": 1789709597117, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 16, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "fork", + "kind": "attractor/fork", + "meta": { + "label": "fork", + "shape": "component", + "kind": "parallel", + "classes": [], + "span": { + "line": 5, + "column": 5 + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "fork", + "branches": 2 + } + }, + "recorded_at": 1789709597117, + "record": { + "seq": 16, + "origin": "core", + "recorded_at": 1789709597117, + "body": { + "event": "route.applied", + "kind": "edge", + "firing": 2, + "group": 0, + "edge": 6 + } + }, + "derived": { + "target": { + "id": 3, + "name": "a", + "kind": "attractor/branch", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "a", + "index": 0 + }, + "synthetic": true + } + }, + "transition": "Continue", + "back": false + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 43, + "kind": "petri", + "id": "execution 0/16/1", + "recorded_at": 1789709597117, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 16, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "fork", + "kind": "attractor/fork", + "meta": { + "label": "fork", + "shape": "component", + "kind": "parallel", + "classes": [], + "span": { + "line": 5, + "column": 5 + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "fork", + "branches": 2 + } + }, + "recorded_at": 1789709597117, + "derived": { + "event": "fork.started", + "occurrence": { + "execution": 0, + "fork": 2, + "firing": 2, + "visit": 1, + "generation": 0 + }, + "branches": [ + { + "fork": 2, + "index": 0 + }, + { + "fork": 2, + "index": 1 + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 44, + "kind": "petri", + "id": "execution 0/17/0", + "recorded_at": 1789709597117, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 17, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "fork", + "kind": "attractor/fork", + "meta": { + "label": "fork", + "shape": "component", + "kind": "parallel", + "classes": [], + "span": { + "line": 5, + "column": 5 + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "fork", + "branches": 2 + } + }, + "recorded_at": 1789709597117, + "record": { + "seq": 17, + "origin": "core", + "recorded_at": 1789709597117, + "body": { + "event": "route.applied", + "kind": "edge", + "firing": 2, + "group": 1, + "edge": 7 + } + }, + "derived": { + "target": { + "id": 4, + "name": "b", + "kind": "attractor/branch", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "b", + "index": 1 + }, + "synthetic": true + } + }, + "transition": "Continue", + "back": false + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 45, + "kind": "petri", + "id": "execution 0/18/0", + "recorded_at": 1789709597117, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 18, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "fork", + "kind": "attractor/fork", + "meta": { + "label": "fork", + "shape": "component", + "kind": "parallel", + "classes": [], + "span": { + "line": 5, + "column": 5 + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "fork", + "branches": 2 + } + }, + "recorded_at": 1789709597117, + "record": { + "seq": 18, + "origin": "core", + "recorded_at": 1789709597117, + "body": { + "event": "token.emitted", + "edge": 6, + "generation": 0, + "payload": { + "snapshot": { + "failure_class": "", + "internal.run_id": "petri" + }, + "nodes": null, + "occurrence": { + "fork": "fork", + "firing": 2 + } + }, + "from": 2 + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 46, + "kind": "petri", + "id": "execution 0/19/0", + "recorded_at": 1789709597117, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 19, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 2, + "name": "fork", + "kind": "attractor/fork", + "meta": { + "label": "fork", + "shape": "component", + "kind": "parallel", + "classes": [], + "span": { + "line": 5, + "column": 5 + } + } + }, + "firing": 2, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "fork", + "branches": 2 + } + }, + "recorded_at": 1789709597117, + "record": { + "seq": 19, + "origin": "core", + "recorded_at": 1789709597117, + "body": { + "event": "token.emitted", + "edge": 7, + "generation": 0, + "payload": { + "snapshot": { + "failure_class": "", + "internal.run_id": "petri" + }, + "nodes": null, + "occurrence": { + "fork": "fork", + "firing": 2 + } + }, + "from": 2 + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 47, + "kind": "platform", + "id": "8", + "recorded_at": 1789709597117, + "item": { + "seq": 8, + "recorded_at": 1789709597117, + "record": { + "kind": "checkpoint", + "execution": 0, + "firing": 2, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "beeaef66193b2dc4b53a704b6293e68e0db5aa27", + "operation": { + "execution": 0, + "decision": { + "attempt_start": { + "firing": 2, + "attempt": 1 + } + }, + "effect": "checkpoint" + } + }, + "position": { + "execution": 0, + "firing": 2 + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 48, + "kind": "petri", + "id": "coordinator/6/0", + "recorded_at": 1789709597117, + "item": { + "id": { + "log": "coordinator", + "seq": 6, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 1, + "parent": { + "execution": 0, + "firing": 4, + "attempt": 1, + "slot": "branch:fork@2:1:b" + } + }, + "recorded_at": 1789709597117, + "record": { + "seq": 6, + "origin": "external", + "recorded_at": 1789709597117, + "body": { + "event": "invocation.declared", + "invocation": 1, + "call": { + "parent": 0, + "firing": 4, + "attempt": 1, + "slot": "branch:fork@2:1:b" + }, + "graph": "940ba00308da213651f5dfcfbe78408688ae4d5deb9f96a4356536e2ddf51883", + "context": { + "failure_class": "", + "internal.run_id": "petri" + }, + "secret_bindings": "inherit", + "sandbox": { + "inherited": { + "lease": 0 + } + }, + "admission": { + "gate": "fork@0", + "max_parallel": 4 + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 49, + "kind": "petri", + "id": "execution 0/20/0", + "recorded_at": 1789709597117, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 20, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 3, + "name": "a", + "kind": "attractor/branch", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "a", + "index": 0 + }, + "synthetic": true + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597117, + "record": { + "seq": 20, + "origin": "external", + "recorded_at": 1789709597117, + "body": { + "event": "admission.decided", + "decision_id": { + "attempt_start": { + "firing": 3, + "attempt": 1 + } + }, + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 50, + "kind": "petri", + "id": "execution 0/21/0", + "recorded_at": 1789709597117, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 21, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 3, + "name": "a", + "kind": "attractor/branch", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "a", + "index": 0 + }, + "synthetic": true + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597117, + "record": { + "seq": 21, + "origin": "external", + "recorded_at": 1789709597117, + "body": { + "event": "step.started", + "firing": 3, + "attempt": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 51, + "kind": "petri", + "id": "execution 0/21/1", + "recorded_at": 1789709597117, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 21, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 3, + "name": "a", + "kind": "attractor/branch", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "a", + "index": 0 + }, + "synthetic": true + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597117, + "derived": { + "event": "wait.state.changed", + "state": "running" + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 52, + "kind": "petri", + "id": "execution 0/22/0", + "recorded_at": 1789709597117, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 22, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "b", + "kind": "attractor/branch", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "b", + "index": 1 + }, + "synthetic": true + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597117, + "record": { + "seq": 22, + "origin": "external", + "recorded_at": 1789709597117, + "body": { + "event": "admission.decided", + "decision_id": { + "attempt_start": { + "firing": 4, + "attempt": 1 + } + }, + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 53, + "kind": "petri", + "id": "execution 0/23/0", + "recorded_at": 1789709597117, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 23, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "b", + "kind": "attractor/branch", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "b", + "index": 1 + }, + "synthetic": true + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597117, + "record": { + "seq": 23, + "origin": "external", + "recorded_at": 1789709597117, + "body": { + "event": "step.started", + "firing": 4, + "attempt": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 54, + "kind": "petri", + "id": "execution 0/23/1", + "recorded_at": 1789709597117, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 23, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "b", + "kind": "attractor/branch", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "b", + "index": 1 + }, + "synthetic": true + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597117, + "derived": { + "event": "wait.state.changed", + "state": "running" + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 55, + "kind": "petri", + "id": "coordinator/7/0", + "recorded_at": 1789709597119, + "item": { + "id": { + "log": "coordinator", + "seq": 7, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 2, + "parent": { + "execution": 0, + "firing": 3, + "attempt": 1, + "slot": "branch:fork@2:0:a" + } + }, + "recorded_at": 1789709597119, + "record": { + "seq": 7, + "origin": "external", + "recorded_at": 1789709597119, + "body": { + "event": "invocation.declared", + "invocation": 2, + "call": { + "parent": 0, + "firing": 3, + "attempt": 1, + "slot": "branch:fork@2:0:a" + }, + "graph": "2b8e4e9c97881bdf04d48f57815fc6633959c953fbc786c7f0ce76488d718542", + "context": { + "failure_class": "", + "internal.run_id": "petri" + }, + "secret_bindings": "inherit", + "sandbox": { + "inherited": { + "lease": 0 + } + }, + "admission": { + "gate": "fork@0", + "max_parallel": 4 + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 56, + "kind": "petri", + "id": "coordinator/8/0", + "recorded_at": 1789709597119, + "item": { + "id": { + "log": "coordinator", + "seq": 8, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 1, + "execution": 1, + "parent": { + "execution": 0, + "firing": 4, + "attempt": 1, + "slot": "branch:fork@2:1:b" + } + }, + "recorded_at": 1789709597119, + "record": { + "seq": 8, + "origin": "external", + "recorded_at": 1789709597119, + "body": { + "event": "execution.declared", + "execution": 1, + "invocation": 1, + "predecessor": null, + "start": { + "entry": "graph_entries", + "context": { + "failure_class": "", + "internal.run_id": "petri" + }, + "prior_firings": {}, + "execution_index": 0, + "max_executions": 32 + }, + "middleware_state": { + "circuit-breaker": [ + 1, + { + "loop_signatures": {}, + "restart_signatures": {}, + "pending": {} + } + ] + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 57, + "kind": "petri", + "id": "execution 0/24/0", + "recorded_at": 1789709597120, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 24, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "b", + "kind": "attractor/branch", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "b", + "index": 1 + }, + "synthetic": true + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597120, + "record": { + "seq": 24, + "origin": "external", + "recorded_at": 1789709597120, + "body": { + "event": "step.progress.recorded", + "firing": 4, + "ev": { + "custom": { + "fork": "fork", + "occurrence": { + "fork": "fork", + "firing": 2 + }, + "branch": "b", + "index": 1, + "item_label": null, + "kind": "attractor.parallel.branch.started", + "invocation": 1 + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 58, + "kind": "petri", + "id": "execution 1/0/0", + "recorded_at": 1789709597120, + "item": { + "id": { + "log": "execution", + "execution": 1, + "seq": 0, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 1, + "execution": 1, + "parent": { + "execution": 0, + "firing": 4, + "attempt": 1, + "slot": "branch:fork@2:1:b" + } + }, + "recorded_at": 1789709597120, + "record": { + "seq": 0, + "origin": "external", + "recorded_at": 1789709597120, + "body": { + "event": "execution.started", + "entry": "graph_entries", + "context": { + "failure_class": "", + "internal.run_id": "petri" + }, + "prior_firings": {}, + "execution_index": 0, + "max_executions": 32 + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 59, + "kind": "petri", + "id": "execution 1/1/0", + "recorded_at": 1789709597120, + "item": { + "id": { + "log": "execution", + "execution": 1, + "seq": 1, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 1, + "execution": 1, + "parent": { + "execution": 0, + "firing": 4, + "attempt": 1, + "slot": "branch:fork@2:1:b" + } + }, + "recorded_at": 1789709597120, + "record": { + "seq": 1, + "origin": "external", + "recorded_at": 1789709597120, + "body": { + "event": "admission.decided", + "decision_id": "execution_start", + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 60, + "kind": "petri", + "id": "execution 1/1/1", + "recorded_at": 1789709597120, + "item": { + "id": { + "log": "execution", + "execution": 1, + "seq": 1, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 1, + "execution": 1, + "parent": { + "execution": 0, + "firing": 4, + "attempt": 1, + "slot": "branch:fork@2:1:b" + } + }, + "subject": { + "node": { + "id": 0, + "name": "b", + "kind": "attractor/command", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch_role": { + "fork": 2, + "index": 1 + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597120, + "derived": { + "event": "visit.started", + "inputs": [ + { + "edge": 0, + "generation": 0, + "payload": null, + "from": 0 + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 61, + "kind": "petri", + "id": "execution 1/1/2", + "recorded_at": 1789709597120, + "item": { + "id": { + "log": "execution", + "execution": 1, + "seq": 1, + "index": 2 + }, + "origin": "derived", + "context": { + "invocation": 1, + "execution": 1, + "parent": { + "execution": 0, + "firing": 4, + "attempt": 1, + "slot": "branch:fork@2:1:b" + } + }, + "subject": { + "node": { + "id": 0, + "name": "b", + "kind": "attractor/command", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch_role": { + "fork": 2, + "index": 1 + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597120, + "derived": { + "event": "wait.state.changed", + "state": "awaiting_admission" + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 62, + "kind": "petri", + "id": "execution 1/2/0", + "recorded_at": 1789709597120, + "item": { + "id": { + "log": "execution", + "execution": 1, + "seq": 2, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 1, + "execution": 1, + "parent": { + "execution": 0, + "firing": 4, + "attempt": 1, + "slot": "branch:fork@2:1:b" + } + }, + "recorded_at": 1789709597120, + "record": { + "seq": 2, + "origin": "core", + "recorded_at": 1789709597120, + "body": { + "event": "token.emitted", + "edge": 0, + "generation": 0, + "payload": null, + "from": 0 + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 63, + "kind": "petri", + "id": "execution 1/3/0", + "recorded_at": 1789709597120, + "item": { + "id": { + "log": "execution", + "execution": 1, + "seq": 3, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 1, + "execution": 1, + "parent": { + "execution": 0, + "firing": 4, + "attempt": 1, + "slot": "branch:fork@2:1:b" + } + }, + "subject": { + "node": { + "id": 0, + "name": "b", + "kind": "attractor/command", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch_role": { + "fork": 2, + "index": 1 + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597120, + "record": { + "seq": 3, + "origin": "external", + "recorded_at": 1789709597120, + "body": { + "event": "admission.decided", + "decision_id": { + "attempt_start": { + "firing": 1, + "attempt": 1 + } + }, + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 64, + "kind": "petri", + "id": "execution 1/4/0", + "recorded_at": 1789709597120, + "item": { + "id": { + "log": "execution", + "execution": 1, + "seq": 4, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 1, + "execution": 1, + "parent": { + "execution": 0, + "firing": 4, + "attempt": 1, + "slot": "branch:fork@2:1:b" + } + }, + "subject": { + "node": { + "id": 0, + "name": "b", + "kind": "attractor/command", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch_role": { + "fork": 2, + "index": 1 + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597120, + "record": { + "seq": 4, + "origin": "external", + "recorded_at": 1789709597120, + "body": { + "event": "step.started", + "firing": 1, + "attempt": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 65, + "kind": "petri", + "id": "execution 1/4/1", + "recorded_at": 1789709597120, + "item": { + "id": { + "log": "execution", + "execution": 1, + "seq": 4, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 1, + "execution": 1, + "parent": { + "execution": 0, + "firing": 4, + "attempt": 1, + "slot": "branch:fork@2:1:b" + } + }, + "subject": { + "node": { + "id": 0, + "name": "b", + "kind": "attractor/command", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch_role": { + "fork": 2, + "index": 1 + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597120, + "derived": { + "event": "wait.state.changed", + "state": "running" + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 66, + "kind": "petri", + "id": "coordinator/9/0", + "recorded_at": 1789709597120, + "item": { + "id": { + "log": "coordinator", + "seq": 9, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 2, + "execution": 2, + "parent": { + "execution": 0, + "firing": 3, + "attempt": 1, + "slot": "branch:fork@2:0:a" + } + }, + "recorded_at": 1789709597120, + "record": { + "seq": 9, + "origin": "external", + "recorded_at": 1789709597120, + "body": { + "event": "execution.declared", + "execution": 2, + "invocation": 2, + "predecessor": null, + "start": { + "entry": "graph_entries", + "context": { + "failure_class": "", + "internal.run_id": "petri" + }, + "prior_firings": {}, + "execution_index": 0, + "max_executions": 32 + }, + "middleware_state": { + "circuit-breaker": [ + 1, + { + "loop_signatures": {}, + "restart_signatures": {}, + "pending": {} + } + ] + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 67, + "kind": "petri", + "id": "execution 0/25/0", + "recorded_at": 1789709597122, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 25, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 3, + "name": "a", + "kind": "attractor/branch", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "a", + "index": 0 + }, + "synthetic": true + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597122, + "record": { + "seq": 25, + "origin": "external", + "recorded_at": 1789709597122, + "body": { + "event": "step.progress.recorded", + "firing": 3, + "ev": { + "custom": { + "fork": "fork", + "occurrence": { + "fork": "fork", + "firing": 2 + }, + "branch": "a", + "index": 0, + "item_label": null, + "kind": "attractor.parallel.branch.started", + "invocation": 2 + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 68, + "kind": "petri", + "id": "execution 2/0/0", + "recorded_at": 1789709597122, + "item": { + "id": { + "log": "execution", + "execution": 2, + "seq": 0, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 2, + "execution": 2, + "parent": { + "execution": 0, + "firing": 3, + "attempt": 1, + "slot": "branch:fork@2:0:a" + } + }, + "recorded_at": 1789709597122, + "record": { + "seq": 0, + "origin": "external", + "recorded_at": 1789709597122, + "body": { + "event": "execution.started", + "entry": "graph_entries", + "context": { + "failure_class": "", + "internal.run_id": "petri" + }, + "prior_firings": {}, + "execution_index": 0, + "max_executions": 32 + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 69, + "kind": "petri", + "id": "execution 2/1/0", + "recorded_at": 1789709597122, + "item": { + "id": { + "log": "execution", + "execution": 2, + "seq": 1, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 2, + "execution": 2, + "parent": { + "execution": 0, + "firing": 3, + "attempt": 1, + "slot": "branch:fork@2:0:a" + } + }, + "recorded_at": 1789709597122, + "record": { + "seq": 1, + "origin": "external", + "recorded_at": 1789709597122, + "body": { + "event": "admission.decided", + "decision_id": "execution_start", + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 70, + "kind": "petri", + "id": "execution 2/1/1", + "recorded_at": 1789709597122, + "item": { + "id": { + "log": "execution", + "execution": 2, + "seq": 1, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 2, + "execution": 2, + "parent": { + "execution": 0, + "firing": 3, + "attempt": 1, + "slot": "branch:fork@2:0:a" + } + }, + "subject": { + "node": { + "id": 0, + "name": "a", + "kind": "attractor/command", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch_role": { + "fork": 2, + "index": 0 + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597122, + "derived": { + "event": "visit.started", + "inputs": [ + { + "edge": 0, + "generation": 0, + "payload": null, + "from": 0 + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 71, + "kind": "petri", + "id": "execution 2/1/2", + "recorded_at": 1789709597122, + "item": { + "id": { + "log": "execution", + "execution": 2, + "seq": 1, + "index": 2 + }, + "origin": "derived", + "context": { + "invocation": 2, + "execution": 2, + "parent": { + "execution": 0, + "firing": 3, + "attempt": 1, + "slot": "branch:fork@2:0:a" + } + }, + "subject": { + "node": { + "id": 0, + "name": "a", + "kind": "attractor/command", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch_role": { + "fork": 2, + "index": 0 + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597122, + "derived": { + "event": "wait.state.changed", + "state": "awaiting_admission" + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 72, + "kind": "petri", + "id": "execution 2/2/0", + "recorded_at": 1789709597122, + "item": { + "id": { + "log": "execution", + "execution": 2, + "seq": 2, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 2, + "execution": 2, + "parent": { + "execution": 0, + "firing": 3, + "attempt": 1, + "slot": "branch:fork@2:0:a" + } + }, + "recorded_at": 1789709597122, + "record": { + "seq": 2, + "origin": "core", + "recorded_at": 1789709597122, + "body": { + "event": "token.emitted", + "edge": 0, + "generation": 0, + "payload": null, + "from": 0 + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 73, + "kind": "petri", + "id": "execution 2/3/0", + "recorded_at": 1789709597122, + "item": { + "id": { + "log": "execution", + "execution": 2, + "seq": 3, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 2, + "execution": 2, + "parent": { + "execution": 0, + "firing": 3, + "attempt": 1, + "slot": "branch:fork@2:0:a" + } + }, + "subject": { + "node": { + "id": 0, + "name": "a", + "kind": "attractor/command", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch_role": { + "fork": 2, + "index": 0 + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597122, + "record": { + "seq": 3, + "origin": "external", + "recorded_at": 1789709597122, + "body": { + "event": "admission.decided", + "decision_id": { + "attempt_start": { + "firing": 1, + "attempt": 1 + } + }, + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 74, + "kind": "petri", + "id": "execution 2/4/0", + "recorded_at": 1789709597122, + "item": { + "id": { + "log": "execution", + "execution": 2, + "seq": 4, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 2, + "execution": 2, + "parent": { + "execution": 0, + "firing": 3, + "attempt": 1, + "slot": "branch:fork@2:0:a" + } + }, + "subject": { + "node": { + "id": 0, + "name": "a", + "kind": "attractor/command", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch_role": { + "fork": 2, + "index": 0 + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597122, + "record": { + "seq": 4, + "origin": "external", + "recorded_at": 1789709597122, + "body": { + "event": "step.started", + "firing": 1, + "attempt": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 75, + "kind": "petri", + "id": "execution 2/4/1", + "recorded_at": 1789709597122, + "item": { + "id": { + "log": "execution", + "execution": 2, + "seq": 4, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 2, + "execution": 2, + "parent": { + "execution": 0, + "firing": 3, + "attempt": 1, + "slot": "branch:fork@2:0:a" + } + }, + "subject": { + "node": { + "id": 0, + "name": "a", + "kind": "attractor/command", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch_role": { + "fork": 2, + "index": 0 + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597122, + "derived": { + "event": "wait.state.changed", + "state": "running" + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 76, + "kind": "platform", + "id": "9", + "recorded_at": 1789709597157, + "item": { + "seq": 9, + "recorded_at": 1789709597157, + "record": { + "kind": "run.notice", + "level": "info", + "code": "test.between_branches", + "message": "recorded while both branches ran" + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 77, + "kind": "petri", + "id": "execution 1/5/0", + "recorded_at": 1789709597190, + "item": { + "id": { + "log": "execution", + "execution": 1, + "seq": 5, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 1, + "execution": 1, + "parent": { + "execution": 0, + "firing": 4, + "attempt": 1, + "slot": "branch:fork@2:1:b" + } + }, + "subject": { + "node": { + "id": 0, + "name": "b", + "kind": "attractor/command", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch_role": { + "fork": 2, + "index": 1 + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597190, + "record": { + "seq": 5, + "origin": "external", + "recorded_at": 1789709597190, + "body": { + "event": "step.progress.recorded", + "firing": 1, + "ev": { + "log": { + "stream": "stdout", + "line": "b" + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 78, + "kind": "petri", + "id": "execution 2/5/0", + "recorded_at": 1789709597190, + "item": { + "id": { + "log": "execution", + "execution": 2, + "seq": 5, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 2, + "execution": 2, + "parent": { + "execution": 0, + "firing": 3, + "attempt": 1, + "slot": "branch:fork@2:0:a" + } + }, + "subject": { + "node": { + "id": 0, + "name": "a", + "kind": "attractor/command", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch_role": { + "fork": 2, + "index": 0 + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597190, + "record": { + "seq": 5, + "origin": "external", + "recorded_at": 1789709597190, + "body": { + "event": "step.progress.recorded", + "firing": 1, + "ev": { + "log": { + "stream": "stdout", + "line": "a" + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 79, + "kind": "petri", + "id": "execution 2/6/0", + "recorded_at": 1789709597317, + "item": { + "id": { + "log": "execution", + "execution": 2, + "seq": 6, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 2, + "execution": 2, + "parent": { + "execution": 0, + "firing": 3, + "attempt": 1, + "slot": "branch:fork@2:0:a" + } + }, + "subject": { + "node": { + "id": 0, + "name": "a", + "kind": "attractor/command", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch_role": { + "fork": 2, + "index": 0 + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597317, + "record": { + "seq": 6, + "origin": "external", + "recorded_at": 1789709597317, + "body": { + "event": "step.progress.recorded", + "firing": 1, + "ev": { + "custom": { + "$note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 2, + "firing": 1, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "abda2b70b2a2627e829428c1d0aa50169b7b17f8", + "reused": false + } + } + } + } + } + }, + "derived": { + "parsed": { + "kind": "note", + "note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 2, + "firing": 1, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "abda2b70b2a2627e829428c1d0aa50169b7b17f8", + "reused": false + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 80, + "kind": "petri", + "id": "execution 2/7/0", + "recorded_at": 1789709597317, + "item": { + "id": { + "log": "execution", + "execution": 2, + "seq": 7, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 2, + "execution": 2, + "parent": { + "execution": 0, + "firing": 3, + "attempt": 1, + "slot": "branch:fork@2:0:a" + } + }, + "subject": { + "node": { + "id": 0, + "name": "a", + "kind": "attractor/command", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch_role": { + "fork": 2, + "index": 0 + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597317, + "record": { + "seq": 7, + "origin": "external", + "recorded_at": 1789709597317, + "body": { + "event": "step.finished", + "firing": 1, + "attempt": 1, + "outcome": { + "status": "success", + "output": { + "exit_status": 0, + "stdout": "a\n", + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 96 + }, + "context_updates": { + "command.output": "a\n", + "failure_class": "" + } + } + } + }, + "derived": { + "final": true, + "exhausted": false + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 81, + "kind": "petri", + "id": "execution 2/7/1", + "recorded_at": 1789709597317, + "item": { + "id": { + "log": "execution", + "execution": 2, + "seq": 7, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 2, + "execution": 2, + "parent": { + "execution": 0, + "firing": 3, + "attempt": 1, + "slot": "branch:fork@2:0:a" + } + }, + "subject": { + "node": { + "id": 0, + "name": "a", + "kind": "attractor/command", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch_role": { + "fork": 2, + "index": 0 + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597317, + "derived": { + "event": "visit.completed", + "outcome": { + "status": "success", + "output": { + "exit_status": 0, + "stdout": "a\n", + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 96 + }, + "context_updates": { + "command.output": "a\n", + "failure_class": "" + } + }, + "executed": true, + "attempts": 1 + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 82, + "kind": "petri", + "id": "execution 2/8/0", + "recorded_at": 1789709597317, + "item": { + "id": { + "log": "execution", + "execution": 2, + "seq": 8, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 2, + "execution": 2, + "parent": { + "execution": 0, + "firing": 3, + "attempt": 1, + "slot": "branch:fork@2:0:a" + } + }, + "subject": { + "node": { + "id": 0, + "name": "a", + "kind": "attractor/command", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch_role": { + "fork": 2, + "index": 0 + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597317, + "record": { + "seq": 8, + "origin": "external", + "recorded_at": 1789709597317, + "body": { + "event": "routing.resolved", + "decision_id": { + "route": { + "firing": 1, + "attempt": 1 + } + }, + "groups": [] + } + }, + "derived": { + "groups": [] + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 83, + "kind": "platform", + "id": "10", + "recorded_at": 1789709597317, + "item": { + "seq": 10, + "recorded_at": 1789709597317, + "record": { + "kind": "checkpoint", + "execution": 2, + "firing": 1, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "abda2b70b2a2627e829428c1d0aa50169b7b17f8", + "operation": { + "execution": 2, + "decision": { + "attempt_start": { + "firing": 1, + "attempt": 1 + } + }, + "effect": "checkpoint" + } + }, + "position": { + "execution": 2, + "firing": 1 + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 84, + "kind": "petri", + "id": "coordinator/10/0", + "recorded_at": 1789709597317, + "item": { + "id": { + "log": "coordinator", + "seq": 10, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 2, + "execution": 2, + "parent": { + "execution": 0, + "firing": 3, + "attempt": 1, + "slot": "branch:fork@2:0:a" + } + }, + "recorded_at": 1789709597317, + "record": { + "seq": 10, + "origin": "external", + "recorded_at": 1789709597317, + "body": { + "event": "execution.finished", + "execution": 2, + "exit": { + "terminal": { + "status": "success" + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 85, + "kind": "petri", + "id": "coordinator/11/0", + "recorded_at": 1789709597317, + "item": { + "id": { + "log": "coordinator", + "seq": 11, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 2, + "execution": 2, + "parent": { + "execution": 0, + "firing": 3, + "attempt": 1, + "slot": "branch:fork@2:0:a" + } + }, + "recorded_at": 1789709597317, + "record": { + "seq": 11, + "origin": "external", + "recorded_at": 1789709597317, + "body": { + "event": "invocation.finished", + "invocation": 2, + "result": { + "status": "success", + "failure": null, + "final_execution": 2, + "output": { + "exit_status": 0, + "stdout": "a\n", + "outcome": "succeeded", + "failure_class": "" + }, + "context": { + "command.output": "a\n", + "failure_class": "", + "internal.run_id": "petri" + }, + "updates": { + "command.output": "a\n", + "failure_class": "" + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 86, + "kind": "petri", + "id": "execution 0/26/0", + "recorded_at": 1789709597318, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 26, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 3, + "name": "a", + "kind": "attractor/branch", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "a", + "index": 0 + }, + "synthetic": true + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597318, + "record": { + "seq": 26, + "origin": "external", + "recorded_at": 1789709597318, + "body": { + "event": "step.progress.recorded", + "firing": 3, + "ev": { + "custom": { + "fork": "fork", + "occurrence": { + "fork": "fork", + "firing": 2 + }, + "branch": "a", + "index": 0, + "item_label": null, + "kind": "attractor.parallel.branch.completed", + "invocation": 2, + "status": "succeeded", + "disposition": "completed", + "started": true, + "duration_ms": 200 + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 87, + "kind": "petri", + "id": "execution 1/6/0", + "recorded_at": 1789709597433, + "item": { + "id": { + "log": "execution", + "execution": 1, + "seq": 6, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 1, + "execution": 1, + "parent": { + "execution": 0, + "firing": 4, + "attempt": 1, + "slot": "branch:fork@2:1:b" + } + }, + "subject": { + "node": { + "id": 0, + "name": "b", + "kind": "attractor/command", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch_role": { + "fork": 2, + "index": 1 + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597433, + "record": { + "seq": 6, + "origin": "external", + "recorded_at": 1789709597433, + "body": { + "event": "step.progress.recorded", + "firing": 1, + "ev": { + "custom": { + "$note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 1, + "firing": 1, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "572c40a80a2b6456e7dd68d63a0fce6416805fab", + "reused": false + } + } + } + } + } + }, + "derived": { + "parsed": { + "kind": "note", + "note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 1, + "firing": 1, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "572c40a80a2b6456e7dd68d63a0fce6416805fab", + "reused": false + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 88, + "kind": "petri", + "id": "execution 1/7/0", + "recorded_at": 1789709597433, + "item": { + "id": { + "log": "execution", + "execution": 1, + "seq": 7, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 1, + "execution": 1, + "parent": { + "execution": 0, + "firing": 4, + "attempt": 1, + "slot": "branch:fork@2:1:b" + } + }, + "subject": { + "node": { + "id": 0, + "name": "b", + "kind": "attractor/command", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch_role": { + "fork": 2, + "index": 1 + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597433, + "record": { + "seq": 7, + "origin": "external", + "recorded_at": 1789709597433, + "body": { + "event": "step.finished", + "firing": 1, + "attempt": 1, + "outcome": { + "status": "success", + "output": { + "exit_status": 0, + "stdout": "b\n", + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 98 + }, + "context_updates": { + "command.output": "b\n", + "failure_class": "" + } + } + } + }, + "derived": { + "final": true, + "exhausted": false + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 89, + "kind": "petri", + "id": "execution 1/7/1", + "recorded_at": 1789709597433, + "item": { + "id": { + "log": "execution", + "execution": 1, + "seq": 7, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 1, + "execution": 1, + "parent": { + "execution": 0, + "firing": 4, + "attempt": 1, + "slot": "branch:fork@2:1:b" + } + }, + "subject": { + "node": { + "id": 0, + "name": "b", + "kind": "attractor/command", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch_role": { + "fork": 2, + "index": 1 + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597433, + "derived": { + "event": "visit.completed", + "outcome": { + "status": "success", + "output": { + "exit_status": 0, + "stdout": "b\n", + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 98 + }, + "context_updates": { + "command.output": "b\n", + "failure_class": "" + } + }, + "executed": true, + "attempts": 1 + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 90, + "kind": "petri", + "id": "execution 1/8/0", + "recorded_at": 1789709597433, + "item": { + "id": { + "log": "execution", + "execution": 1, + "seq": 8, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 1, + "execution": 1, + "parent": { + "execution": 0, + "firing": 4, + "attempt": 1, + "slot": "branch:fork@2:1:b" + } + }, + "subject": { + "node": { + "id": 0, + "name": "b", + "kind": "attractor/command", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "command", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch_role": { + "fork": 2, + "index": 1 + } + } + }, + "firing": 1, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597433, + "record": { + "seq": 8, + "origin": "external", + "recorded_at": 1789709597433, + "body": { + "event": "routing.resolved", + "decision_id": { + "route": { + "firing": 1, + "attempt": 1 + } + }, + "groups": [] + } + }, + "derived": { + "groups": [] + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 91, + "kind": "platform", + "id": "11", + "recorded_at": 1789709597433, + "item": { + "seq": 11, + "recorded_at": 1789709597433, + "record": { + "kind": "checkpoint", + "execution": 1, + "firing": 1, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "572c40a80a2b6456e7dd68d63a0fce6416805fab", + "operation": { + "execution": 1, + "decision": { + "attempt_start": { + "firing": 1, + "attempt": 1 + } + }, + "effect": "checkpoint" + } + }, + "position": { + "execution": 1, + "firing": 1 + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 92, + "kind": "petri", + "id": "coordinator/12/0", + "recorded_at": 1789709597434, + "item": { + "id": { + "log": "coordinator", + "seq": 12, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 1, + "execution": 1, + "parent": { + "execution": 0, + "firing": 4, + "attempt": 1, + "slot": "branch:fork@2:1:b" + } + }, + "recorded_at": 1789709597434, + "record": { + "seq": 12, + "origin": "external", + "recorded_at": 1789709597434, + "body": { + "event": "execution.finished", + "execution": 1, + "exit": { + "terminal": { + "status": "success" + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 93, + "kind": "petri", + "id": "coordinator/13/0", + "recorded_at": 1789709597434, + "item": { + "id": { + "log": "coordinator", + "seq": 13, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 1, + "execution": 1, + "parent": { + "execution": 0, + "firing": 4, + "attempt": 1, + "slot": "branch:fork@2:1:b" + } + }, + "recorded_at": 1789709597434, + "record": { + "seq": 13, + "origin": "external", + "recorded_at": 1789709597434, + "body": { + "event": "invocation.finished", + "invocation": 1, + "result": { + "status": "success", + "failure": null, + "final_execution": 1, + "output": { + "exit_status": 0, + "stdout": "b\n", + "outcome": "succeeded", + "failure_class": "" + }, + "context": { + "command.output": "b\n", + "failure_class": "", + "internal.run_id": "petri" + }, + "updates": { + "command.output": "b\n", + "failure_class": "" + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 94, + "kind": "petri", + "id": "execution 0/27/0", + "recorded_at": 1789709597434, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 27, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "b", + "kind": "attractor/branch", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "b", + "index": 1 + }, + "synthetic": true + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597434, + "record": { + "seq": 27, + "origin": "external", + "recorded_at": 1789709597434, + "body": { + "event": "step.progress.recorded", + "firing": 4, + "ev": { + "custom": { + "fork": "fork", + "occurrence": { + "fork": "fork", + "firing": 2 + }, + "branch": "b", + "index": 1, + "item_label": null, + "kind": "attractor.parallel.branch.completed", + "invocation": 1, + "status": "succeeded", + "disposition": "completed", + "started": true, + "duration_ms": 316 + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 95, + "kind": "petri", + "id": "execution 0/28/0", + "recorded_at": 1789709597525, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 28, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 3, + "name": "a", + "kind": "attractor/branch", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "a", + "index": 0 + }, + "synthetic": true + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597525, + "record": { + "seq": 28, + "origin": "external", + "recorded_at": 1789709597525, + "body": { + "event": "step.progress.recorded", + "firing": 3, + "ev": { + "custom": { + "$note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 3, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "d19179e7075e6d260660832c2f26e3a31efecd20", + "reused": false + } + } + } + } + } + }, + "derived": { + "parsed": { + "kind": "note", + "note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 3, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "d19179e7075e6d260660832c2f26e3a31efecd20", + "reused": false + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 96, + "kind": "petri", + "id": "execution 0/29/0", + "recorded_at": 1789709597525, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 29, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 3, + "name": "a", + "kind": "attractor/branch", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "a", + "index": 0 + }, + "synthetic": true + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597525, + "record": { + "seq": 29, + "origin": "external", + "recorded_at": 1789709597525, + "body": { + "event": "step.finished", + "firing": 3, + "attempt": 1, + "outcome": { + "status": "success", + "output": { + "id": "a", + "index": 0, + "status": "succeeded", + "context_updates": { + "command.output": "a\n" + } + }, + "metrics": { + "duration_ms": 200 + } + } + } + }, + "derived": { + "final": true, + "exhausted": false + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 97, + "kind": "petri", + "id": "execution 0/29/1", + "recorded_at": 1789709597525, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 29, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 3, + "name": "a", + "kind": "attractor/branch", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "a", + "index": 0 + }, + "synthetic": true + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597525, + "derived": { + "event": "visit.completed", + "outcome": { + "status": "success", + "output": { + "id": "a", + "index": 0, + "status": "succeeded", + "context_updates": { + "command.output": "a\n" + } + }, + "metrics": { + "duration_ms": 200 + } + }, + "executed": true, + "attempts": 1 + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 98, + "kind": "petri", + "id": "execution 0/30/0", + "recorded_at": 1789709597525, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 30, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 3, + "name": "a", + "kind": "attractor/branch", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "a", + "index": 0 + }, + "synthetic": true + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597525, + "record": { + "seq": 30, + "origin": "external", + "recorded_at": 1789709597525, + "body": { + "event": "routing.resolved", + "decision_id": { + "route": { + "firing": 3, + "attempt": 1 + } + }, + "groups": [ + { + "group": 0, + "draw": null, + "trace": [], + "decision": { + "emit": 4 + } + } + ] + } + }, + "derived": { + "groups": [ + { + "group": 0, + "target": { + "id": 5, + "name": "merge", + "kind": "attractor/fan_in", + "meta": { + "label": "merge", + "shape": "tripleoctagon", + "kind": "parallel.fan_in", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "3": { + "to": "exit", + "label": null + } + } + } + } + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 99, + "kind": "petri", + "id": "execution 0/31/0", + "recorded_at": 1789709597525, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 31, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 3, + "name": "a", + "kind": "attractor/branch", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "a", + "index": 0 + }, + "synthetic": true + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597525, + "record": { + "seq": 31, + "origin": "core", + "recorded_at": 1789709597525, + "body": { + "event": "route.applied", + "kind": "edge", + "firing": 3, + "group": 0, + "edge": 4 + } + }, + "derived": { + "target": { + "id": 5, + "name": "merge", + "kind": "attractor/fan_in", + "meta": { + "label": "merge", + "shape": "tripleoctagon", + "kind": "parallel.fan_in", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "3": { + "to": "exit", + "label": null + } + } + } + }, + "transition": "Continue", + "back": false + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 100, + "kind": "petri", + "id": "execution 0/32/0", + "recorded_at": 1789709597525, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 32, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 3, + "name": "a", + "kind": "attractor/branch", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "a", + "index": 0 + }, + "synthetic": true + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597525, + "record": { + "seq": 32, + "origin": "core", + "recorded_at": 1789709597525, + "body": { + "event": "token.emitted", + "edge": 4, + "generation": 0, + "payload": { + "index": 0, + "value": { + "id": "a", + "index": 0, + "status": "succeeded", + "context_updates": { + "command.output": "a\n" + } + }, + "occurrence": { + "fork": "fork", + "firing": 2 + } + }, + "from": 3 + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 101, + "kind": "platform", + "id": "12", + "recorded_at": 1789709597525, + "item": { + "seq": 12, + "recorded_at": 1789709597525, + "record": { + "kind": "checkpoint", + "execution": 0, + "firing": 3, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "d19179e7075e6d260660832c2f26e3a31efecd20", + "operation": { + "execution": 0, + "decision": { + "attempt_start": { + "firing": 3, + "attempt": 1 + } + }, + "effect": "checkpoint" + } + }, + "position": { + "execution": 0, + "firing": 3 + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 102, + "kind": "petri", + "id": "execution 0/33/0", + "recorded_at": 1789709597618, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 33, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "b", + "kind": "attractor/branch", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "b", + "index": 1 + }, + "synthetic": true + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597618, + "record": { + "seq": 33, + "origin": "external", + "recorded_at": 1789709597618, + "body": { + "event": "step.progress.recorded", + "firing": 4, + "ev": { + "custom": { + "$note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 4, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "9606890ac1f59546498ea7167131f9e542edd05f", + "reused": false + } + } + } + } + } + }, + "derived": { + "parsed": { + "kind": "note", + "note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 4, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "9606890ac1f59546498ea7167131f9e542edd05f", + "reused": false + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 103, + "kind": "petri", + "id": "execution 0/34/0", + "recorded_at": 1789709597618, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 34, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "b", + "kind": "attractor/branch", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "b", + "index": 1 + }, + "synthetic": true + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597618, + "record": { + "seq": 34, + "origin": "external", + "recorded_at": 1789709597618, + "body": { + "event": "step.finished", + "firing": 4, + "attempt": 1, + "outcome": { + "status": "success", + "output": { + "id": "b", + "index": 1, + "status": "succeeded", + "context_updates": { + "command.output": "b\n" + } + }, + "metrics": { + "duration_ms": 316 + } + } + } + }, + "derived": { + "final": true, + "exhausted": false + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 104, + "kind": "petri", + "id": "execution 0/34/1", + "recorded_at": 1789709597618, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 34, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "b", + "kind": "attractor/branch", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "b", + "index": 1 + }, + "synthetic": true + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597618, + "derived": { + "event": "visit.completed", + "outcome": { + "status": "success", + "output": { + "id": "b", + "index": 1, + "status": "succeeded", + "context_updates": { + "command.output": "b\n" + } + }, + "metrics": { + "duration_ms": 316 + } + }, + "executed": true, + "attempts": 1 + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 105, + "kind": "petri", + "id": "execution 0/35/0", + "recorded_at": 1789709597618, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 35, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "b", + "kind": "attractor/branch", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "b", + "index": 1 + }, + "synthetic": true + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597618, + "record": { + "seq": 35, + "origin": "external", + "recorded_at": 1789709597618, + "body": { + "event": "routing.resolved", + "decision_id": { + "route": { + "firing": 4, + "attempt": 1 + } + }, + "groups": [ + { + "group": 0, + "draw": null, + "trace": [], + "decision": { + "emit": 5 + } + } + ] + } + }, + "derived": { + "groups": [ + { + "group": 0, + "target": { + "id": 5, + "name": "merge", + "kind": "attractor/fan_in", + "meta": { + "label": "merge", + "shape": "tripleoctagon", + "kind": "parallel.fan_in", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "3": { + "to": "exit", + "label": null + } + } + } + } + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 106, + "kind": "petri", + "id": "execution 0/35/1", + "recorded_at": 1789709597618, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 35, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 3, + "name": "a", + "kind": "attractor/branch", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "a", + "index": 0 + }, + "synthetic": true + } + }, + "firing": 3, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 0 + } + }, + "recorded_at": 1789709597618, + "derived": { + "event": "branch.completed", + "occurrence": { + "execution": 0, + "fork": 2, + "firing": 2, + "visit": 1, + "generation": 0 + }, + "result": { + "branch": { + "fork": 2, + "index": 0 + }, + "node": { + "id": 3, + "name": "a", + "kind": "attractor/branch", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "a", + "index": 0 + }, + "synthetic": true + } + }, + "firing": 3, + "status": "success", + "payload": { + "index": 0, + "value": { + "id": "a", + "index": 0, + "status": "succeeded", + "context_updates": { + "command.output": "a\n" + } + }, + "occurrence": { + "fork": "fork", + "firing": 2 + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 107, + "kind": "petri", + "id": "execution 0/35/2", + "recorded_at": 1789709597618, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 35, + "index": 2 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "b", + "kind": "attractor/branch", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "b", + "index": 1 + }, + "synthetic": true + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597618, + "derived": { + "event": "branch.completed", + "occurrence": { + "execution": 0, + "fork": 2, + "firing": 2, + "visit": 1, + "generation": 0 + }, + "result": { + "branch": { + "fork": 2, + "index": 1 + }, + "node": { + "id": 4, + "name": "b", + "kind": "attractor/branch", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "b", + "index": 1 + }, + "synthetic": true + } + }, + "firing": 4, + "status": "success", + "payload": { + "index": 1, + "value": { + "id": "b", + "index": 1, + "status": "succeeded", + "context_updates": { + "command.output": "b\n" + } + }, + "occurrence": { + "fork": "fork", + "firing": 2 + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 108, + "kind": "petri", + "id": "execution 0/35/3", + "recorded_at": 1789709597618, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 35, + "index": 3 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 5, + "name": "merge", + "kind": "attractor/fan_in", + "meta": { + "label": "merge", + "shape": "tripleoctagon", + "kind": "parallel.fan_in", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "3": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 5, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "join", + "fork": 2 + } + }, + "recorded_at": 1789709597618, + "derived": { + "event": "fork.completed", + "occurrence": { + "execution": 0, + "fork": 2, + "firing": 2, + "visit": 1, + "generation": 0 + }, + "fork": { + "id": 2, + "name": "fork", + "kind": "attractor/fork", + "meta": { + "label": "fork", + "shape": "component", + "kind": "parallel", + "classes": [], + "span": { + "line": 5, + "column": 5 + } + } + }, + "results": [ + { + "branch": { + "fork": 2, + "index": 0 + }, + "node": { + "id": 3, + "name": "a", + "kind": "attractor/branch", + "meta": { + "label": "a", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 6, + "column": 5 + }, + "edges": { + "1": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "a", + "index": 0 + }, + "synthetic": true + } + }, + "firing": 3, + "status": "success", + "payload": { + "index": 0, + "value": { + "id": "a", + "index": 0, + "status": "succeeded", + "context_updates": { + "command.output": "a\n" + } + }, + "occurrence": { + "fork": "fork", + "firing": 2 + } + } + }, + { + "branch": { + "fork": 2, + "index": 1 + }, + "node": { + "id": 4, + "name": "b", + "kind": "attractor/branch", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "b", + "index": 1 + }, + "synthetic": true + } + }, + "firing": 4, + "status": "success", + "payload": { + "index": 1, + "value": { + "id": "b", + "index": 1, + "status": "succeeded", + "context_updates": { + "command.output": "b\n" + } + }, + "occurrence": { + "fork": "fork", + "firing": 2 + } + } + } + ], + "disposition": "joined" + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 109, + "kind": "petri", + "id": "execution 0/35/4", + "recorded_at": 1789709597618, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 35, + "index": 4 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 5, + "name": "merge", + "kind": "attractor/fan_in", + "meta": { + "label": "merge", + "shape": "tripleoctagon", + "kind": "parallel.fan_in", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "3": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 5, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "join", + "fork": 2 + } + }, + "recorded_at": 1789709597618, + "derived": { + "event": "visit.started", + "inputs": [ + { + "edge": 4, + "generation": 0, + "payload": { + "index": 0, + "value": { + "id": "a", + "index": 0, + "status": "succeeded", + "context_updates": { + "command.output": "a\n" + } + }, + "occurrence": { + "fork": "fork", + "firing": 2 + } + }, + "from": 3 + }, + { + "edge": 5, + "generation": 0, + "payload": { + "index": 1, + "value": { + "id": "b", + "index": 1, + "status": "succeeded", + "context_updates": { + "command.output": "b\n" + } + }, + "occurrence": { + "fork": "fork", + "firing": 2 + } + }, + "from": 4 + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 110, + "kind": "petri", + "id": "execution 0/35/5", + "recorded_at": 1789709597618, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 35, + "index": 5 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 5, + "name": "merge", + "kind": "attractor/fan_in", + "meta": { + "label": "merge", + "shape": "tripleoctagon", + "kind": "parallel.fan_in", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "3": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 5, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "join", + "fork": 2 + } + }, + "recorded_at": 1789709597618, + "derived": { + "event": "wait.state.changed", + "state": "awaiting_admission" + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 111, + "kind": "petri", + "id": "execution 0/36/0", + "recorded_at": 1789709597618, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 36, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "b", + "kind": "attractor/branch", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "b", + "index": 1 + }, + "synthetic": true + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597618, + "record": { + "seq": 36, + "origin": "core", + "recorded_at": 1789709597618, + "body": { + "event": "route.applied", + "kind": "edge", + "firing": 4, + "group": 0, + "edge": 5 + } + }, + "derived": { + "target": { + "id": 5, + "name": "merge", + "kind": "attractor/fan_in", + "meta": { + "label": "merge", + "shape": "tripleoctagon", + "kind": "parallel.fan_in", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "3": { + "to": "exit", + "label": null + } + } + } + }, + "transition": "Continue", + "back": false + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 112, + "kind": "petri", + "id": "execution 0/37/0", + "recorded_at": 1789709597618, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 37, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 4, + "name": "b", + "kind": "attractor/branch", + "meta": { + "label": "b", + "shape": "parallelogram", + "kind": "parallel.branch", + "classes": [], + "span": { + "line": 7, + "column": 5 + }, + "edges": { + "2": { + "to": "merge", + "label": null + } + }, + "branch": { + "fork": "fork", + "target": "b", + "index": 1 + }, + "synthetic": true + } + }, + "firing": 4, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "member", + "fork": 2, + "index": 1 + } + }, + "recorded_at": 1789709597618, + "record": { + "seq": 37, + "origin": "core", + "recorded_at": 1789709597618, + "body": { + "event": "token.emitted", + "edge": 5, + "generation": 0, + "payload": { + "index": 1, + "value": { + "id": "b", + "index": 1, + "status": "succeeded", + "context_updates": { + "command.output": "b\n" + } + }, + "occurrence": { + "fork": "fork", + "firing": 2 + } + }, + "from": 4 + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 113, + "kind": "petri", + "id": "execution 0/38/0", + "recorded_at": 1789709597618, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 38, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 5, + "name": "merge", + "kind": "attractor/fan_in", + "meta": { + "label": "merge", + "shape": "tripleoctagon", + "kind": "parallel.fan_in", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "3": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 5, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "join", + "fork": 2 + } + }, + "recorded_at": 1789709597618, + "record": { + "seq": 38, + "origin": "external", + "recorded_at": 1789709597618, + "body": { + "event": "admission.decided", + "decision_id": { + "attempt_start": { + "firing": 5, + "attempt": 1 + } + }, + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 114, + "kind": "petri", + "id": "execution 0/39/0", + "recorded_at": 1789709597618, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 39, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 5, + "name": "merge", + "kind": "attractor/fan_in", + "meta": { + "label": "merge", + "shape": "tripleoctagon", + "kind": "parallel.fan_in", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "3": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 5, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "join", + "fork": 2 + } + }, + "recorded_at": 1789709597618, + "record": { + "seq": 39, + "origin": "external", + "recorded_at": 1789709597618, + "body": { + "event": "step.started", + "firing": 5, + "attempt": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 115, + "kind": "petri", + "id": "execution 0/39/1", + "recorded_at": 1789709597618, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 39, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 5, + "name": "merge", + "kind": "attractor/fan_in", + "meta": { + "label": "merge", + "shape": "tripleoctagon", + "kind": "parallel.fan_in", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "3": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 5, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "join", + "fork": 2 + } + }, + "recorded_at": 1789709597618, + "derived": { + "event": "wait.state.changed", + "state": "running" + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 116, + "kind": "petri", + "id": "execution 0/40/0", + "recorded_at": 1789709597618, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 40, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 5, + "name": "merge", + "kind": "attractor/fan_in", + "meta": { + "label": "merge", + "shape": "tripleoctagon", + "kind": "parallel.fan_in", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "3": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 5, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "join", + "fork": 2 + } + }, + "recorded_at": 1789709597618, + "record": { + "seq": 40, + "origin": "external", + "recorded_at": 1789709597618, + "body": { + "event": "step.progress.recorded", + "firing": 5, + "ev": { + "custom": { + "kind": "attractor.parallel.completed", + "node": "merge", + "fork": "fork", + "occurrence": { + "fork": "fork", + "firing": 2 + }, + "branch_count": 2, + "success_count": 2, + "failure_count": 0, + "status": "succeeded" + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 117, + "kind": "platform", + "id": "13", + "recorded_at": 1789709597618, + "item": { + "seq": 13, + "recorded_at": 1789709597618, + "record": { + "kind": "checkpoint", + "execution": 0, + "firing": 4, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "9606890ac1f59546498ea7167131f9e542edd05f", + "operation": { + "execution": 0, + "decision": { + "attempt_start": { + "firing": 4, + "attempt": 1 + } + }, + "effect": "checkpoint" + } + }, + "position": { + "execution": 0, + "firing": 4 + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 118, + "kind": "petri", + "id": "execution 0/41/0", + "recorded_at": 1789709597709, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 41, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 5, + "name": "merge", + "kind": "attractor/fan_in", + "meta": { + "label": "merge", + "shape": "tripleoctagon", + "kind": "parallel.fan_in", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "3": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 5, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "join", + "fork": 2 + } + }, + "recorded_at": 1789709597709, + "record": { + "seq": 41, + "origin": "external", + "recorded_at": 1789709597709, + "body": { + "event": "step.progress.recorded", + "firing": 5, + "ev": { + "custom": { + "$note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 5, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "f99391338a6f1b460ca44095fed82a92dfe7e0a0", + "reused": false + } + } + } + } + } + }, + "derived": { + "parsed": { + "kind": "note", + "note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 5, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "f99391338a6f1b460ca44095fed82a92dfe7e0a0", + "reused": false + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 119, + "kind": "petri", + "id": "execution 0/42/0", + "recorded_at": 1789709597709, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 42, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 5, + "name": "merge", + "kind": "attractor/fan_in", + "meta": { + "label": "merge", + "shape": "tripleoctagon", + "kind": "parallel.fan_in", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "3": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 5, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "join", + "fork": 2 + } + }, + "recorded_at": 1789709597709, + "record": { + "seq": 42, + "origin": "external", + "recorded_at": 1789709597709, + "body": { + "event": "step.finished", + "firing": 5, + "attempt": 1, + "outcome": { + "status": "success", + "output": [ + { + "id": "a", + "index": 0, + "status": "succeeded", + "context_updates": { + "command.output": "a\n" + } + }, + { + "id": "b", + "index": 1, + "status": "succeeded", + "context_updates": { + "command.output": "b\n" + } + } + ], + "metrics": { + "duration_ms": 0 + }, + "context_updates": { + "parallel.branch_count": 2, + "parallel.results": [ + { + "id": "a", + "index": 0, + "status": "succeeded", + "context_updates": { + "command.output": "a\n" + } + }, + { + "id": "b", + "index": 1, + "status": "succeeded", + "context_updates": { + "command.output": "b\n" + } + } + ] + } + } + } + }, + "derived": { + "final": true, + "exhausted": false + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 120, + "kind": "petri", + "id": "execution 0/42/1", + "recorded_at": 1789709597709, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 42, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 5, + "name": "merge", + "kind": "attractor/fan_in", + "meta": { + "label": "merge", + "shape": "tripleoctagon", + "kind": "parallel.fan_in", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "3": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 5, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "join", + "fork": 2 + } + }, + "recorded_at": 1789709597709, + "derived": { + "event": "visit.completed", + "outcome": { + "status": "success", + "output": [ + { + "id": "a", + "index": 0, + "status": "succeeded", + "context_updates": { + "command.output": "a\n" + } + }, + { + "id": "b", + "index": 1, + "status": "succeeded", + "context_updates": { + "command.output": "b\n" + } + } + ], + "metrics": { + "duration_ms": 0 + }, + "context_updates": { + "parallel.branch_count": 2, + "parallel.results": [ + { + "id": "a", + "index": 0, + "status": "succeeded", + "context_updates": { + "command.output": "a\n" + } + }, + { + "id": "b", + "index": 1, + "status": "succeeded", + "context_updates": { + "command.output": "b\n" + } + } + ] + } + }, + "executed": true, + "attempts": 1 + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 121, + "kind": "petri", + "id": "execution 0/43/0", + "recorded_at": 1789709597709, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 43, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 5, + "name": "merge", + "kind": "attractor/fan_in", + "meta": { + "label": "merge", + "shape": "tripleoctagon", + "kind": "parallel.fan_in", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "3": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 5, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "join", + "fork": 2 + } + }, + "recorded_at": 1789709597709, + "record": { + "seq": 43, + "origin": "external", + "recorded_at": 1789709597709, + "body": { + "event": "routing.resolved", + "decision_id": { + "route": { + "firing": 5, + "attempt": 1 + } + }, + "groups": [ + { + "group": 0, + "draw": null, + "trace": [], + "decision": { + "emit": 3 + } + } + ] + } + }, + "derived": { + "groups": [ + { + "group": 0, + "target": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + } + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 122, + "kind": "petri", + "id": "execution 0/43/1", + "recorded_at": 1789709597709, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 43, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 6, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597709, + "derived": { + "event": "visit.started", + "inputs": [ + { + "edge": 3, + "generation": 0, + "payload": [ + { + "id": "a", + "index": 0, + "status": "succeeded", + "context_updates": { + "command.output": "a\n" + } + }, + { + "id": "b", + "index": 1, + "status": "succeeded", + "context_updates": { + "command.output": "b\n" + } + } + ], + "from": 5 + } + ] + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 123, + "kind": "petri", + "id": "execution 0/43/2", + "recorded_at": 1789709597709, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 43, + "index": 2 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 6, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597709, + "derived": { + "event": "wait.state.changed", + "state": "awaiting_admission" + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 124, + "kind": "petri", + "id": "execution 0/44/0", + "recorded_at": 1789709597709, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 44, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 5, + "name": "merge", + "kind": "attractor/fan_in", + "meta": { + "label": "merge", + "shape": "tripleoctagon", + "kind": "parallel.fan_in", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "3": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 5, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "join", + "fork": 2 + } + }, + "recorded_at": 1789709597709, + "record": { + "seq": 44, + "origin": "core", + "recorded_at": 1789709597709, + "body": { + "event": "route.applied", + "kind": "edge", + "firing": 5, + "group": 0, + "edge": 3 + } + }, + "derived": { + "target": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "transition": "Continue", + "back": false + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 125, + "kind": "petri", + "id": "execution 0/45/0", + "recorded_at": 1789709597709, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 45, + "index": 0 + }, + "origin": "core", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 5, + "name": "merge", + "kind": "attractor/fan_in", + "meta": { + "label": "merge", + "shape": "tripleoctagon", + "kind": "parallel.fan_in", + "classes": [], + "span": { + "line": 8, + "column": 5 + }, + "edges": { + "3": { + "to": "exit", + "label": null + } + } + } + }, + "firing": 5, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "join", + "fork": 2 + } + }, + "recorded_at": 1789709597709, + "record": { + "seq": 45, + "origin": "core", + "recorded_at": 1789709597709, + "body": { + "event": "token.emitted", + "edge": 3, + "generation": 0, + "payload": [ + { + "id": "a", + "index": 0, + "status": "succeeded", + "context_updates": { + "command.output": "a\n" + } + }, + { + "id": "b", + "index": 1, + "status": "succeeded", + "context_updates": { + "command.output": "b\n" + } + } + ], + "from": 5 + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 126, + "kind": "petri", + "id": "execution 0/46/0", + "recorded_at": 1789709597709, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 46, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 6, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597709, + "record": { + "seq": 46, + "origin": "external", + "recorded_at": 1789709597709, + "body": { + "event": "admission.decided", + "decision_id": { + "attempt_start": { + "firing": 6, + "attempt": 1 + } + }, + "decision": "admit", + "trace": [] + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 127, + "kind": "petri", + "id": "execution 0/47/0", + "recorded_at": 1789709597709, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 47, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 6, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597709, + "record": { + "seq": 47, + "origin": "external", + "recorded_at": 1789709597709, + "body": { + "event": "step.started", + "firing": 6, + "attempt": 1 + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 128, + "kind": "petri", + "id": "execution 0/47/1", + "recorded_at": 1789709597709, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 47, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 6, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597709, + "derived": { + "event": "wait.state.changed", + "state": "running" + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 129, + "kind": "platform", + "id": "14", + "recorded_at": 1789709597709, + "item": { + "seq": 14, + "recorded_at": 1789709597709, + "record": { + "kind": "checkpoint", + "execution": 0, + "firing": 5, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "f99391338a6f1b460ca44095fed82a92dfe7e0a0", + "operation": { + "execution": 0, + "decision": { + "attempt_start": { + "firing": 5, + "attempt": 1 + } + }, + "effect": "checkpoint" + } + }, + "position": { + "execution": 0, + "firing": 5 + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 130, + "kind": "petri", + "id": "coordinator/14/0", + "recorded_at": 1789709597798, + "item": { + "id": { + "log": "coordinator", + "seq": 14, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709597798, + "record": { + "seq": 14, + "origin": "external", + "recorded_at": 1789709597798, + "body": { + "event": "execution.finished", + "execution": 0, + "exit": { + "terminal": { + "status": "success" + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 131, + "kind": "petri", + "id": "coordinator/15/0", + "recorded_at": 1789709597798, + "item": { + "id": { + "log": "coordinator", + "seq": 15, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "recorded_at": 1789709597798, + "record": { + "seq": 15, + "origin": "external", + "recorded_at": 1789709597798, + "body": { + "event": "invocation.finished", + "invocation": 0, + "result": { + "status": "success", + "failure": null, + "final_execution": 0, + "output": null, + "context": { + "failure_class": "", + "internal.run_id": "petri", + "parallel.branch_count": 2, + "parallel.results": [ + { + "id": "a", + "index": 0, + "status": "succeeded", + "context_updates": { + "command.output": "a\n" + } + }, + { + "id": "b", + "index": 1, + "status": "succeeded", + "context_updates": { + "command.output": "b\n" + } + } + ] + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 132, + "kind": "petri", + "id": "execution 0/48/0", + "recorded_at": 1789709597798, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 48, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 6, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597798, + "record": { + "seq": 48, + "origin": "external", + "recorded_at": 1789709597798, + "body": { + "event": "step.progress.recorded", + "firing": 6, + "ev": { + "custom": { + "$note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 6, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "a1f942a57396c23c378fde187c43b15adbb7e648", + "reused": false + } + } + } + } + } + }, + "derived": { + "parsed": { + "kind": "note", + "note": { + "kind": "fabro.checkpoint", + "payload": { + "execution": 0, + "firing": 6, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "a1f942a57396c23c378fde187c43b15adbb7e648", + "reused": false + } + } + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 133, + "kind": "petri", + "id": "execution 0/49/0", + "recorded_at": 1789709597798, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 49, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 6, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597798, + "record": { + "seq": 49, + "origin": "external", + "recorded_at": 1789709597798, + "body": { + "event": "step.finished", + "firing": 6, + "attempt": 1, + "outcome": { + "status": "success", + "output": { + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 0 + }, + "context_updates": { + "failure_class": "" + } + } + } + }, + "derived": { + "final": true, + "exhausted": false + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 134, + "kind": "petri", + "id": "execution 0/49/1", + "recorded_at": 1789709597798, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 49, + "index": 1 + }, + "origin": "derived", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 6, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597798, + "derived": { + "event": "visit.completed", + "outcome": { + "status": "success", + "output": { + "outcome": "succeeded", + "failure_class": "" + }, + "metrics": { + "duration_ms": 0 + }, + "context_updates": { + "failure_class": "" + } + }, + "executed": true, + "attempts": 1 + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 135, + "kind": "petri", + "id": "execution 0/50/0", + "recorded_at": 1789709597798, + "item": { + "id": { + "log": "execution", + "execution": 0, + "seq": 50, + "index": 0 + }, + "origin": "external", + "context": { + "invocation": 0, + "execution": 0 + }, + "subject": { + "node": { + "id": 1, + "name": "exit", + "kind": "attractor/stage", + "meta": { + "label": "exit", + "shape": "Msquare", + "kind": "exit", + "classes": [], + "span": { + "line": 4, + "column": 5 + } + } + }, + "firing": 6, + "visit": 1, + "attempt": 1, + "generation": 0, + "branch": { + "role": "none" + } + }, + "recorded_at": 1789709597798, + "record": { + "seq": 50, + "origin": "external", + "recorded_at": 1789709597798, + "body": { + "event": "routing.resolved", + "decision_id": { + "route": { + "firing": 6, + "attempt": 1 + } + }, + "groups": [] + } + }, + "derived": { + "groups": [] + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 136, + "kind": "platform", + "id": "15", + "recorded_at": 1789709597798, + "item": { + "seq": 15, + "recorded_at": 1789709597798, + "record": { + "kind": "checkpoint", + "execution": 0, + "firing": 6, + "attempt": 1, + "workspace": "invocation-0-scope-0", + "git_commit_sha": "a1f942a57396c23c378fde187c43b15adbb7e648", + "operation": { + "execution": 0, + "decision": { + "attempt_start": { + "firing": 6, + "attempt": 1 + } + }, + "effect": "checkpoint" + } + }, + "position": { + "execution": 0, + "firing": 6 + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 137, + "kind": "petri", + "id": "coordinator/16/0", + "recorded_at": 1789709597838, + "item": { + "id": { + "log": "coordinator", + "seq": 16, + "index": 0 + }, + "origin": "external", + "context": {}, + "recorded_at": 1789709597838, + "record": { + "seq": 16, + "origin": "external", + "recorded_at": 1789709597838, + "body": { + "event": "run.finished", + "status": "success" + } + } + } + }, + { + "run_id": "01M2SG2E0BQSRJ1G31WRPX542Y", + "stream_seq": 138, + "kind": "platform", + "id": "16", + "recorded_at": 1789709597843, + "item": { + "seq": 16, + "recorded_at": 1789709597843, + "record": { + "kind": "run.lifecycle", + "transition": "succeeded", + "status": { + "kind": "succeeded", + "reason": "completed" + } + } + } + } + ] +} \ No newline at end of file diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 305efece0..dc22dd1fc 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -2970,23 +2970,40 @@ paths: tags: [Run Internals] summary: List Run Events description: | - Returns a paginated JSON list of stored run events. Ascending order - uses `since_seq` as an inclusive cursor. Descending order uses + Returns a paginated JSON list of the run's events. The shape depends + on the engine the run was created for (`RunSpec.engine`). + + For a legacy run (`engine.kind = legacy`): stored run events in the + legacy envelope (`PaginatedEventList`). Ascending order uses + `since_seq` as an inclusive cursor. Descending order uses `before_seq` as an exclusive cursor and starts at the newest event when `before_seq` is omitted. + + For a Petri run (`engine.kind = petri`): the run stream + (`PaginatedRunStreamList`), one ordered delivery of Petri's own + `RunEvent`s and Fabro's platform records in the `RunStreamItem` + envelope, in `stream_seq` order. The cursor is `after`: the last + `stream_seq` the client saw, exclusive; the first page is `after=0`. + `since_seq`, `before_seq` and `order` are not accepted for a Petri + run. A client that reconnects resumes from its last `stream_seq` and + deduplicates by each item's `id`; every item is delivered once, in + order, with no gap. parameters: - $ref: "#/components/parameters/RunId" - $ref: "#/components/parameters/SinceSeq" - $ref: "#/components/parameters/EventLimit" - $ref: "#/components/parameters/BeforeSeq" - $ref: "#/components/parameters/EventOrder" + - $ref: "#/components/parameters/StreamAfter" responses: "200": - description: Paginated list of run events + description: Paginated list of run events, in the run engine's envelope content: application/json: schema: - $ref: "#/components/schemas/PaginatedEventList" + oneOf: + - $ref: "#/components/schemas/PaginatedEventList" + - $ref: "#/components/schemas/PaginatedRunStreamList" "400": description: Invalid cursor and order combination headers: @@ -3097,10 +3114,25 @@ paths: operationId: attachRunEvents tags: [Run Internals] summary: Attach Run Events - description: Opens an ordered server-sent event stream starting at `since_seq`, replaying persisted events and continuing with live updates while the run remains active. + description: | + Opens an ordered server-sent event stream, replaying persisted items + and continuing with live updates while the run remains active. Each + `data:` frame is one JSON object in the run engine's envelope. + + For a legacy run the frames are `EventEnvelope`s and the stream + starts at `since_seq` (inclusive; the next unseen event when omitted). + It ends after `run.completed` or `run.failed`. + + For a Petri run the frames are `RunStreamItem`s and the stream + starts after `after` (the last `stream_seq` the client saw; `0` + replays the whole run; the next unseen item when omitted). It ends + once the run is no longer active and every committed item has been + sent. A reconnecting client passes its last `stream_seq` as `after` + and deduplicates by `id`. parameters: - $ref: "#/components/parameters/RunId" - $ref: "#/components/parameters/SinceSeq" + - $ref: "#/components/parameters/StreamAfter" responses: "200": description: Server-sent event stream @@ -6354,6 +6386,20 @@ components: default: 100 example: 100 + StreamAfter: + name: after + in: query + required: false + description: | + Run stream cursor for a Petri run: the last `stream_seq` the client + saw, exclusive. `0` starts at the first item. + schema: + type: integer + format: uint64 + minimum: 0 + default: 0 + example: 42 + QuestionId: name: qid in: path @@ -10933,6 +10979,99 @@ components: meta: $ref: "#/components/schemas/PaginationMeta" + RunStreamItemKind: + description: Which item shape a run stream item carries. + type: string + enum: [petri, platform] + + RunStreamItem: + description: | + One item of a Petri run's stream: a Petri `RunEvent` or a Fabro + platform record in Fabro's envelope. + + `stream_seq` is the durable per-run delivery sequence the projector + assigned when the item's record was committed: dense, strictly + increasing within the run, and the cursor for `after`. `id` is the + item's own identity, kept beside the cursor so a client deduplicates + by it: for a Petri event the `EventId` as `//` + (`coordinator/3/0`, `execution 1/23/0`); for a platform record its + `seq`. Petri's `EventId` is per log and has no platform variant, so + it is never the cursor. + + A `petri` item is a Petri `RunEvent` passed through unchanged: + `{id: {log, execution?, seq, index}, origin, recorded_at, + observed_at?, context: {invocation, execution, parent?}, subject?, + record?, derived?}`. Its vocabulary is Petri's public event contract + (`crates/core/execution/EVENTS.md` in the Petri repository), not + Fabro's: the recorded event's name is `record.body.event` + (`.`, e.g. `visit.started`, `step.finished`, + `run.finished`), a derived view event's is `derived.event`, and the + stage a subject names is `(context.execution, subject.firing)` with + `subject.node.name` and `subject.visit` as its display label. The + server reports the contract version it serves in + `PaginatedRunStreamList.event_contract_version`. + + A `platform` item is a stored platform record: `{seq, recorded_at, + record: {kind, ...}, position?: {execution, firing}}`. `record.kind` + is one of `run.created`, `run.lifecycle`, `run.title`, `run.parent`, + `run.archived`, `run.unarchived`, `run.superseded`, `run.notice`, + `interview.answered`, `run.branch`, `git.identity`, `checkpoint`, + `pull_request.created`, `notification.sent`, `run.paired`. + type: object + required: + - run_id + - stream_seq + - kind + - id + - recorded_at + - item + properties: + run_id: + type: string + stream_seq: + type: integer + format: uint64 + minimum: 0 + description: The delivery sequence; the cursor. + kind: + $ref: "#/components/schemas/RunStreamItemKind" + id: + type: string + description: The item's own identity, for deduplication. + recorded_at: + type: integer + format: uint64 + minimum: 0 + description: Milliseconds since the Unix epoch when the item's record was appended. + item: + type: object + additionalProperties: true + description: The Petri `RunEvent` or the stored platform record, unchanged. + + PaginatedRunStreamList: + description: | + One page of a Petri run's stream, in `stream_seq` order. + `event_contract_version` is Petri's `EVENT_CONTRACT_VERSION` the + server was built against: the version of the event contract every + `petri` item follows. + type: object + required: + - data + - meta + - event_contract_version + properties: + data: + type: array + items: + $ref: "#/components/schemas/RunStreamItem" + meta: + $ref: "#/components/schemas/PaginationMeta" + event_contract_version: + type: integer + format: uint32 + minimum: 0 + example: 3 + AppendEventResponse: description: Assigned sequence number for an appended event. type: object @@ -12841,6 +12980,58 @@ components: oneOf: - $ref: "#/components/schemas/ForkSourceRef" - type: "null" + engine: + $ref: "#/components/schemas/RunEngine" + description: | + The engine the run was created for, with what it admitted. + Absent in a spec written before the field existed, which means + the legacy executor. + + RunEngine: + description: | + The engine a run was created for. `legacy` is the in-process + executor; `petri` names the Petri workflow engine and carries what + Petri admitted at create time. + oneOf: + - type: object + required: [kind] + properties: + kind: + type: string + enum: [legacy] + - allOf: + - type: object + required: [kind] + properties: + kind: + type: string + enum: [petri] + - $ref: "#/components/schemas/PetriAdmission" + + PetriAdmission: + description: | + What Petri admitted for a run at create time: the lowered root graph + and the pre-lowered child graphs, every one persisted in the blob + store before the run exists. + type: object + required: [graph] + properties: + graph: + $ref: "#/components/schemas/PetriGraphRef" + children: + type: array + items: + $ref: "#/components/schemas/PetriGraphRef" + + PetriGraphRef: + description: An admitted graph in the blob store, verified by digest on load. + type: object + required: [blob, digest] + properties: + blob: + $ref: "#/components/schemas/BlobHash" + digest: + type: string UpdateRunParentRequest: type: object diff --git a/lib/apps/fabro-cli/src/commands/run/attach.rs b/lib/apps/fabro-cli/src/commands/run/attach.rs index e14f3949d..85216054c 100644 --- a/lib/apps/fabro-cli/src/commands/run/attach.rs +++ b/lib/apps/fabro-cli/src/commands/run/attach.rs @@ -31,7 +31,7 @@ use fabro_workflow::run_status::RunStatus; use tokio::signal::ctrl_c; use tokio::time::{Duration as TokioDuration, sleep}; -use super::run_progress; +use super::{petri_stream, run_progress}; use crate::server_client; const INTERVIEW_UNANSWERED_MESSAGE: &str = @@ -39,6 +39,9 @@ const INTERVIEW_UNANSWERED_MESSAGE: &str = const JSON_INTERVIEW_MESSAGE: &str = "This run is waiting for human input, but --json is non-interactive. Reattach without --json to answer it."; const ATTACH_PREMATURE_EOF_MESSAGE: &str = "Attach stream ended before terminal run event."; const PROMPT_READ_POLL_INTERVAL: TokioDuration = TokioDuration::from_millis(50); +/// How long a Petri attach waits before it reconnects to the stream the +/// server ended while the run was still active. +const STREAM_RECONNECT_DELAY: TokioDuration = TokioDuration::from_millis(200); enum PromptRead { Line(String), @@ -182,6 +185,22 @@ pub(crate) async fn attach_run_with_client( ) -> Result { let state = client.get_run_state(run_id).await?; let auto_approve = state.spec.settings.run.execution.approval == ApprovalMode::Auto; + if state.spec.engine.is_petri() { + return Box::pin(attach_petri_run_with_client( + client, + run_id, + &state, + styles, + AttachOptions { + auto_approve, + verbose: live_verbose, + kill_on_detach, + json_output, + }, + printer, + )) + .await; + } let events = client.list_run_events(run_id, None, None).await?; let replay_events = events.clone(); let next_seq = events.last().map_or(1, |event| event.seq.saturating_add(1)); @@ -321,6 +340,197 @@ async fn attach_live_run_with_client( } } +/// Attach to a Petri run: replay its stream through the progress renderer, +/// then follow it live from the last `stream_seq` seen. A question on the +/// stream is asked at the terminal and answered through the questions API. +/// When the server ends the stream before the run's terminal record, the +/// attach reconnects from its cursor, so no item is missed or repeated. +async fn attach_petri_run_with_client( + client: &server_client::Client, + run_id: &RunId, + state: &server_client::RunProjection, + styles: &'static Styles, + opts: AttachOptions, + printer: Printer, +) -> Result { + let is_tty = std::io::stderr().is_terminal(); + let mut progress_ui = run_progress::ProgressUI::new(is_tty, opts.verbose); + let ctrl_c_signal = ctrl_c(); + tokio::pin!(ctrl_c_signal); + + let items = client.list_run_stream(run_id, 0).await?; + let mut cursor = items.last().map_or(0, |item| item.stream_seq); + let mut replayed_exit_code = None; + for item in &items { + emit_stream_item(&mut progress_ui, item, opts.json_output)?; + if let Some(code) = petri_stream::exit_code_of(item) { + replayed_exit_code = Some(ExitCode::from(code)); + } + } + if let Some(exit_code) = replayed_exit_code.or_else(|| { + state_is_terminal(state).then(|| state_exit_code(state).unwrap_or(ExitCode::from(1))) + }) { + finish_progress(&mut progress_ui, opts.json_output); + return Ok(exit_code); + } + + loop { + let mut stream = client.attach_run_stream(run_id, Some(cursor)).await?; + if let Some(exit_code) = Box::pin(handle_pending_petri_interview( + client, + run_id, + &mut stream, + &mut cursor, + &opts, + &mut progress_ui, + styles, + printer, + )) + .await? + { + return Ok(exit_code); + } + + loop { + let next_item = tokio::select! { + _ = &mut ctrl_c_signal => { + handle_detach_signal(client, run_id, opts.kill_on_detach, printer).await; + finish_progress(&mut progress_ui, opts.json_output); + return Ok(ExitCode::from(1)); + } + result = stream.next_item() => result?, + }; + let Some(item) = next_item else { + break; + }; + cursor = item.stream_seq; + emit_stream_item(&mut progress_ui, &item, opts.json_output)?; + if let Some(code) = petri_stream::exit_code_of(&item) { + finish_progress(&mut progress_ui, opts.json_output); + return Ok(ExitCode::from(code)); + } + if petri_stream::question_of(&item).is_some() { + if let Some(exit_code) = Box::pin(handle_pending_petri_interview( + client, + run_id, + &mut stream, + &mut cursor, + &opts, + &mut progress_ui, + styles, + printer, + )) + .await? + { + return Ok(exit_code); + } + } + } + + // The server ended the stream. A run that concluded has nothing + // more to send past what the grace let through; otherwise this is + // a lost connection, and the attach resumes from its cursor. + let state = client.get_run_state(run_id).await?; + if state_is_terminal(&state) { + for item in client.list_run_stream(run_id, cursor).await? { + emit_stream_item(&mut progress_ui, &item, opts.json_output)?; + } + finish_progress(&mut progress_ui, opts.json_output); + return Ok(state_exit_code(&state).unwrap_or(ExitCode::from(1))); + } + sleep(STREAM_RECONNECT_DELAY).await; + } +} + +/// Ask the run's pending question, if one is listed, while the stream keeps +/// flowing: an answer given elsewhere, or the run ending, ends the prompt. +async fn handle_pending_petri_interview( + client: &server_client::Client, + run_id: &RunId, + stream: &mut server_client::RunStreamItemStream, + cursor: &mut u64, + opts: &AttachOptions, + progress_ui: &mut run_progress::ProgressUI, + styles: &'static Styles, + printer: Printer, +) -> Result> { + let Some(question) = client.list_run_questions(run_id).await?.into_iter().next() else { + return Ok(None); + }; + + if json_pending_interview_requires_manual_input(opts.json_output, opts.auto_approve) { + fabro_util::printerr!(printer, "{JSON_INTERVIEW_MESSAGE}"); + return Ok(Some(ExitCode::from(1))); + } + if opts.json_output { + return Ok(None); + } + + hide_progress(progress_ui, opts.json_output); + let ask = ask_attach_question(api_question_to_question(&question), styles); + tokio::pin!(ask); + let ctrl_c_signal = ctrl_c(); + tokio::pin!(ctrl_c_signal); + + let answer = loop { + let next_item = tokio::select! { + answer = &mut ask => { + break answer; + } + _ = &mut ctrl_c_signal => { + handle_detach_signal(client, run_id, opts.kill_on_detach, printer).await; + show_progress(progress_ui, opts.json_output); + return Ok(Some(ExitCode::from(1))); + } + result = stream.next_item() => result?, + }; + + // The stream ended under the prompt: the caller reconnects and asks + // again if the question is still pending. + let Some(item) = next_item else { + show_progress(progress_ui, opts.json_output); + return Ok(None); + }; + + *cursor = item.stream_seq; + emit_stream_item(progress_ui, &item, opts.json_output)?; + + if let Some(code) = petri_stream::exit_code_of(&item) { + show_progress(progress_ui, opts.json_output); + return Ok(Some(ExitCode::from(code))); + } + + if petri_stream::resolves_question(&item, &question.id) { + show_progress(progress_ui, opts.json_output); + return Ok(None); + } + }; + show_progress(progress_ui, opts.json_output); + + if answer_requires_reattach(&answer) { + fabro_util::printerr!(printer, "{INTERVIEW_UNANSWERED_MESSAGE}"); + return Ok(Some(ExitCode::from(1))); + } + + submit_server_interview_answer(client, run_id, &question.id, &answer).await?; + Ok(None) +} + +fn emit_stream_item( + progress_ui: &mut run_progress::ProgressUI, + item: &fabro_types::RunStreamItem, + json_output: bool, +) -> Result<()> { + if json_output { + let stdout = std::io::stdout(); + let mut handle = stdout.lock(); + writeln!(handle, "{}", petri_stream::raw_line(item)?)?; + } else { + progress_ui.handle_stream_item(item); + } + Ok(()) +} + async fn handle_pending_server_interview( client: &server_client::Client, run_id: &RunId, diff --git a/lib/apps/fabro-cli/src/commands/run/events.rs b/lib/apps/fabro-cli/src/commands/run/events.rs index 062c7fc10..ab35b3c05 100644 --- a/lib/apps/fabro-cli/src/commands/run/events.rs +++ b/lib/apps/fabro-cli/src/commands/run/events.rs @@ -20,6 +20,7 @@ use fabro_util::terminal::Styles; use tokio::time; use tracing::{debug, info}; +use super::petri_stream; use crate::args::EventsArgs; use crate::command_context::CommandContext; use crate::server_client; @@ -42,6 +43,24 @@ pub(crate) async fn run( None => None, }; + // A Petri run's events are its stream, in the stream envelope. + let state = client + .get_run_state(&run_id) + .await + .context("Failed to read run state from server")?; + if state.spec.engine.is_petri() { + let pretty = args.pretty && !ctx.json_output(); + return Box::pin(petri_stream::print_events( + client.as_ref(), + &run_id, + args, + since_cutoff, + pretty, + styles, + )) + .await; + } + let events = match (args.tail, since_cutoff.is_none()) { (Some(tail), true) => { // With --tail 0 --follow, fetch one event anyway so `last_seq` diff --git a/lib/apps/fabro-cli/src/commands/run/mod.rs b/lib/apps/fabro-cli/src/commands/run/mod.rs index f59559070..202e75e59 100644 --- a/lib/apps/fabro-cli/src/commands/run/mod.rs +++ b/lib/apps/fabro-cli/src/commands/run/mod.rs @@ -20,6 +20,7 @@ pub(crate) mod fork; pub(crate) mod logs; pub(crate) mod output; pub(crate) mod overrides; +pub(crate) mod petri_stream; mod petri_worker; pub(crate) mod preview; mod remote_workflow; @@ -126,7 +127,7 @@ pub(crate) async fn dispatch( RunCommands::Diff(args) => diff::run(args, base_ctx).await, RunCommands::Events(args) => { let styles = Styles::detect_stdout(); - events::run(&args, &styles, base_ctx).await + Box::pin(events::run(&args, &styles, base_ctx)).await } RunCommands::Logs(args) => logs::run(&args, base_ctx).await, RunCommands::Resume(args) => { diff --git a/lib/apps/fabro-cli/src/commands/run/petri_stream.rs b/lib/apps/fabro-cli/src/commands/run/petri_stream.rs new file mode 100644 index 000000000..5539ad7cc --- /dev/null +++ b/lib/apps/fabro-cli/src/commands/run/petri_stream.rs @@ -0,0 +1,1116 @@ +//! A Petri run's stream as the CLI reads it: `run events` prints the +//! envelope raw or pretty, and `run attach` follows it live from a cursor. +//! +//! The stream is `GET /runs/{id}/events` for a run whose spec names Petri: +//! one ordered delivery of Petri's own events and Fabro's platform records +//! in the `RunStreamItem` envelope, addressed by `stream_seq`. The CLI +//! never names a Petri type; it reads the item as JSON through +//! [`PetriItem`], which knows where Petri's contract keeps the event name, +//! the subject and the parsed progress payloads. + +#![expect( + clippy::disallowed_types, + reason = "sync CLI `run events` output: blocking std::io::Write is the intended mechanism" +)] +#![expect( + clippy::disallowed_methods, + reason = "sync CLI `run events` output: streams lines to std::io::stdout directly" +)] + +use std::collections::HashMap; +use std::fmt::Write as _; +use std::io::{self, Write}; +use std::time::Duration; + +use anyhow::{Context as _, Result}; +use chrono::{DateTime, TimeZone as _, Utc}; +use fabro_redact::redact_jsonl_line; +use fabro_types::{RunId, RunStreamItem, RunStreamItemKind}; +use fabro_util::terminal::Styles; +use serde_json::Value; +use tokio::time; +use tracing::debug; + +use crate::args::EventsArgs; +use crate::server_client; +use crate::shared::{format_duration_ms, format_usd_micros}; + +/// How long a follower waits before it reconnects after the server ended +/// the stream while the run was still active. +const FOLLOW_RECONNECT_DELAY: Duration = Duration::from_millis(200); + +/// One stream item read as Petri's contract lays it out. +#[derive(Clone, Copy)] +pub(crate) struct PetriItem<'a> { + pub(crate) item: &'a RunStreamItem, +} + +impl<'a> PetriItem<'a> { + pub(crate) fn new(item: &'a RunStreamItem) -> Self { + Self { item } + } + + pub(crate) fn is_petri(self) -> bool { + self.item.kind == RunStreamItemKind::Petri + } + + /// The `.` name of a Petri event, or the kind of a + /// platform record. + pub(crate) fn name(self) -> Option<&'a str> { + self.item.name() + } + + pub(crate) fn recorded_at(self) -> DateTime { + millis(self.item.recorded_at) + } + + fn value(self) -> &'a Value { + &self.item.item + } + + /// The recorded event's fields, beside its `event` tag. + pub(crate) fn body(self) -> Option<&'a Value> { + self.value().pointer("/record/body") + } + + pub(crate) fn derived(self) -> Option<&'a Value> { + self.value().get("derived") + } + + /// Petri's reading of a `step.progress.recorded` payload. + pub(crate) fn parsed(self) -> Option<&'a Value> { + self.derived()?.get("parsed") + } + + /// The `custom` payload of a `step.progress.recorded`, when it is one. + pub(crate) fn custom(self) -> Option<&'a Value> { + self.body()?.pointer("/ev/custom") + } + + /// A `step.progress.recorded` log line: `(stream, line)`. + pub(crate) fn log_line(self) -> Option<(&'a str, &'a str)> { + let log = self.body()?.pointer("/ev/log")?; + Some((log.get("stream")?.as_str()?, log.get("line")?.as_str()?)) + } + + pub(crate) fn subject(self) -> Option<&'a Value> { + self.value().get("subject") + } + + pub(crate) fn node(self) -> Option<&'a Value> { + self.subject()?.get("node") + } + + pub(crate) fn node_name(self) -> Option<&'a str> { + self.node()?.get("name")?.as_str() + } + + /// The node's display label: its `meta.label`, else its name. + pub(crate) fn node_label(self) -> Option<&'a str> { + let node = self.node()?; + node.pointer("/meta/label") + .and_then(Value::as_str) + .filter(|label| !label.is_empty()) + .or_else(|| node.get("name")?.as_str()) + } + + pub(crate) fn node_kind(self) -> Option<&'a str> { + self.node()?.pointer("/meta/kind")?.as_str() + } + + /// Whether the subject's node is a logical stage: not a lowering node + /// (`synthetic`) and not a fork's `parallel.branch` delegate. + pub(crate) fn is_shown_stage(self) -> bool { + let Some(node) = self.node() else { + return false; + }; + let synthetic = node + .pointer("/meta/synthetic") + .and_then(Value::as_bool) + .unwrap_or(false); + !synthetic && self.node_kind() != Some("parallel.branch") + } + + pub(crate) fn visit(self) -> u64 { + self.subject() + .and_then(|subject| subject.get("visit")) + .and_then(Value::as_u64) + .unwrap_or(1) + } + + pub(crate) fn firing(self) -> Option { + self.subject()?.get("firing")?.as_u64() + } + + pub(crate) fn execution(self) -> Option { + self.value().pointer("/context/execution")?.as_u64() + } + + /// The stage key: `(execution, firing)`. + pub(crate) fn stage_key(self) -> Option { + Some(format!("{}:{}", self.execution()?, self.firing()?)) + } + + /// The stored platform record, tagged by `kind`. + pub(crate) fn platform_record(self) -> Option<&'a Value> { + (!self.is_petri()) + .then(|| self.value().get("record")) + .flatten() + } + + pub(crate) fn str_at(self, pointer: &str) -> Option<&'a str> { + self.value().pointer(pointer)?.as_str() + } +} + +/// Whether the item is the platform record of the run's terminal lifecycle +/// transition, which ends the attached stream. +pub(crate) fn is_terminal_lifecycle(item: &RunStreamItem) -> bool { + let view = PetriItem::new(item); + view.platform_record().is_some_and(|record| { + record["kind"].as_str() == Some("run.lifecycle") + && matches!( + record["transition"].as_str(), + Some("succeeded" | "failed" | "dead") + ) + }) +} + +/// The exit code the item decides, if it is one that ends the run: the +/// engine's `run.finished`, or the platform record of the terminal +/// lifecycle transition. +pub(crate) fn exit_code_of(item: &RunStreamItem) -> Option { + let view = PetriItem::new(item); + if view.is_petri() { + if view.name() != Some("run.finished") { + return None; + } + return Some(match view.body()?.get("status")?.as_str()? { + "success" => 0, + _ => 1, + }); + } + let record = view.platform_record()?; + if record["kind"].as_str() != Some("run.lifecycle") { + return None; + } + match record["transition"].as_str()? { + "succeeded" => Some(0), + "failed" | "dead" => Some(1), + _ => None, + } +} + +/// The question a `step.progress.recorded` carries, when Petri parsed one: +/// its id and text. +pub(crate) fn question_of(item: &RunStreamItem) -> Option<(&str, &str)> { + let view = PetriItem::new(item); + let parsed = view.parsed()?; + if parsed.get("kind")?.as_str()? != "question" { + return None; + } + let question = parsed.get("question")?; + Some(( + question.get("id")?.as_str()?, + question.get("text")?.as_str()?, + )) +} + +/// Whether the item closes the question with this id: a delivered answer, +/// its expiry, or Fabro's record of who answered. +pub(crate) fn resolves_question(item: &RunStreamItem, question_id: &str) -> bool { + let view = PetriItem::new(item); + if let Some(record) = view.platform_record() { + return record["kind"].as_str() == Some("interview.answered") + && record["question"].as_str() == Some(question_id); + } + match view.name() { + Some("control.requested") => view.str_at("/derived/answer/question") == Some(question_id), + Some("step.progress.recorded") => view.parsed().is_some_and(|parsed| { + parsed["kind"].as_str() == Some("question_expired") + && parsed["question"].as_str() == Some(question_id) + }), + _ => false, + } +} + +/// The raw line `run events` prints for an item: the envelope as JSON, +/// redacted. +pub(crate) fn raw_line(item: &RunStreamItem) -> Result { + let line = serde_json::to_string(item)?; + Ok(redact_jsonl_line(&line)) +} + +/// What the pretty printer remembers between items: when each firing +/// started, and when the run did. +#[derive(Default)] +pub(crate) struct PrettyState { + clock: StageClock, +} + +/// When each firing's visit started, by stage key, so its completion can +/// show a duration. +#[derive(Default)] +pub(crate) struct StageClock { + starts: HashMap, + run_start: Option, +} + +impl StageClock { + /// Note the item and answer how long its stage or the run has been + /// running, when the item ends one. + pub(crate) fn observe(&mut self, item: &RunStreamItem) -> Option { + let view = PetriItem::new(item); + match view.name()? { + "run.started" if view.is_petri() => { + self.run_start = Some(item.recorded_at); + None + } + "visit.started" => { + if let Some(key) = view.stage_key() { + self.starts.insert(key, item.recorded_at); + } + None + } + "visit.completed" | "branch.completed" => { + let key = view.stage_key()?; + let start = self.starts.remove(&key)?; + Some(item.recorded_at.saturating_sub(start)) + } + "run.finished" if view.is_petri() => { + Some(item.recorded_at.saturating_sub(self.run_start?)) + } + _ => None, + } + } +} + +/// The pretty line for an item, or nothing for one the terminal does not +/// show. Petri events render by `.` with the stage's label; +/// platform records by their kind. +pub(crate) fn format_pretty( + item: &RunStreamItem, + styles: &Styles, + state: &mut PrettyState, +) -> Option { + let elapsed = state.clock.observe(item); + let view = PetriItem::new(item); + let ts = view.recorded_at().format("%H:%M:%S").to_string(); + let ts = styles.dim.apply_to(&ts).to_string(); + if let Some(record) = view.platform_record() { + return format_platform_record(&ts, record, styles); + } + // A revisited node shows its visit beside its label, as a stage id does. + let label = match (view.node_label(), view.visit()) { + (Some(label), 1) => label.to_string(), + (Some(label), visit) => format!("{label}@{visit}"), + (None, _) => "?".to_string(), + }; + let label = label.as_str(); + match view.name()? { + "run.started" => Some(format!( + "{ts} {}", + styles.dim.apply_to("Engine: petri run started") + )), + "visit.started" if view.is_shown_stage() => Some(format!( + "{ts} {} {}", + styles.bold_cyan.apply_to("\u{25b6}"), + styles.bold.apply_to(label), + )), + "visit.completed" if view.is_shown_stage() => { + let derived = view.derived()?; + let outcome = derived.get("outcome")?; + let executed = derived + .get("executed") + .and_then(Value::as_bool) + .unwrap_or(true); + let status = outcome.get("status").and_then(Value::as_str).unwrap_or("?"); + let duration = format_duration_ms(elapsed.unwrap_or(0)); + if !executed || status == "skipped" { + return Some(format!( + "{ts} {} {} {}", + styles.dim.apply_to("\u{2298}"), + styles.bold.apply_to(label), + styles.dim.apply_to("skipped"), + )); + } + match status { + "success" | "partial_success" => { + let mut line = format!( + "{ts} {} {}", + styles.green.apply_to("\u{2713}"), + styles.bold.apply_to(label), + ); + if status == "partial_success" { + let _ = write!(line, " {}", styles.yellow.apply_to("partial")); + } + let _ = write!(line, " {duration}"); + if let Some(usage) = usage_summary(outcome, styles) { + let _ = write!(line, " {usage}"); + } + Some(line) + } + other => { + let error = outcome + .pointer("/failure/message") + .and_then(Value::as_str) + .unwrap_or(other); + Some(format!( + "{ts} {} {} {}", + styles.red.apply_to("\u{2717}"), + styles.bold.apply_to(label), + styles.red.apply_to(error), + )) + } + } + } + "retry.scheduled" => { + let derived = view.derived()?; + let attempt = derived.get("next_attempt").and_then(Value::as_u64)?; + let delay = derived + .pointer("/base_delay/secs") + .and_then(Value::as_u64) + .map(|secs| secs.saturating_mul(1000)) + .or_else(|| derived.get("base_delay").and_then(Value::as_u64)) + .unwrap_or(0); + Some(format!( + "{ts} {} {}: retrying (attempt {attempt}, delay {})", + styles.yellow.apply_to("\u{21bb}"), + label, + format_duration_ms(delay), + )) + } + "route.applied" => { + let derived = view.derived()?; + let target = derived.pointer("/target/name").and_then(Value::as_str)?; + let transition = derived + .get("transition") + .and_then(Value::as_str) + .unwrap_or("") + .to_lowercase(); + let back = derived + .get("back") + .and_then(Value::as_bool) + .unwrap_or(false); + let detail = if back { " (loop)" } else { "" }; + // Petri records the route after the next visit started, so the + // line names both ends of the edge. + Some(format!( + "{ts} {} {} {} {}{}", + styles.dim.apply_to(view.node_name().unwrap_or("?")), + styles.dim.apply_to("\u{2192}"), + target, + styles.dim.apply_to(&transition), + styles.dim.apply_to(detail), + )) + } + "fork.started" => { + let branches = view + .derived()? + .get("branches") + .and_then(Value::as_array) + .map_or(0, Vec::len); + Some(format!( + "{ts} {} {} {}", + styles.bold_cyan.apply_to("\u{2442}"), + styles.bold.apply_to(label), + styles.dim.apply_to(format!("{branches} branches")), + )) + } + "branch.completed" => { + let result = view.derived()?.get("result")?; + let name = result + .pointer("/node/name") + .and_then(Value::as_str) + .unwrap_or(label); + let status = result.get("status").and_then(Value::as_str).unwrap_or("?"); + let (glyph, style) = if status == "success" { + ("\u{2713}", &styles.green) + } else { + ("\u{2717}", &styles.red) + }; + Some(format!( + "{ts} {} branch {} {} {}", + style.apply_to(glyph), + name, + styles.dim.apply_to(status), + styles + .dim + .apply_to(format_duration_ms(elapsed.unwrap_or(0))), + )) + } + "fork.completed" => { + let derived = view.derived()?; + let results = derived + .get("results") + .and_then(Value::as_array) + .map_or(0, Vec::len); + let disposition = derived + .get("disposition") + .and_then(Value::as_str) + .unwrap_or("joined"); + Some(format!( + "{ts} {} {} {}", + styles.dim.apply_to("\u{2442}"), + styles + .dim + .apply_to(format!("{results} branches {disposition}")), + styles.bold.apply_to(label), + )) + } + "step.progress.recorded" => format_progress(&ts, view, label, styles), + "control.requested" => { + let answer = view.derived()?.get("answer")?; + let value = answer + .get("choice") + .or_else(|| answer.get("text")) + .and_then(Value::as_str) + .map_or_else(|| answer.to_string(), str::to_string); + let late = !view + .derived()? + .get("deliverable") + .and_then(Value::as_bool) + .unwrap_or(true); + let suffix = if late { " (late)" } else { "" }; + Some(format!( + "{ts} {} answered: {}{}", + styles.dim.apply_to("\u{21b3}"), + value, + styles.dim.apply_to(suffix), + )) + } + "invocation.cancel.requested" => { + let reason = view + .body()? + .get("reason") + .and_then(Value::as_str) + .unwrap_or("?"); + Some(format!( + "{ts} {} {}", + styles.bold_red.apply_to("\u{2717} Cancel requested"), + styles.dim.apply_to(reason), + )) + } + "run.finished" => { + let status = view.body()?.get("status").and_then(Value::as_str)?; + let duration = format_duration_ms(elapsed.unwrap_or(0)); + Some(match status { + "success" => format!( + "{ts} {} {}", + styles.bold_green.apply_to("\u{2713} SUCCEEDED"), + styles.bold.apply_to(&duration), + ), + "cancelled" => format!( + "{ts} {} {}", + styles.bold_red.apply_to("\u{2717} CANCELLED"), + styles.bold.apply_to(&duration), + ), + _ => format!( + "{ts} {} {}", + styles.bold_red.apply_to("\u{2717} FAILED"), + styles.bold.apply_to(&duration), + ), + }) + } + _ => None, + } +} + +/// The token and cost summary of a finished visit, from the Pebble or +/// prompt usage in its metrics. +fn usage_summary(outcome: &Value, styles: &Styles) -> Option { + let custom = outcome.pointer("/metrics/custom")?; + let usage = custom + .get("pebble.usage") + .or_else(|| custom.get("prompt.usage"))?; + let tokens = usage.get("tokens")?; + let input = tokens.get("input").and_then(Value::as_u64).unwrap_or(0); + let output = tokens.get("output").and_then(Value::as_u64).unwrap_or(0); + let total = input.saturating_add(output); + let mut parts = Vec::new(); + if let Some(cost) = usage.pointer("/cost/usd_micros").and_then(Value::as_u64) { + parts.push(format_usd_micros(cost)); + } + if total > 0 { + parts.push(format!("{total} toks")); + } + (!parts.is_empty()).then(|| styles.dim.apply_to(parts.join(" ")).to_string()) +} + +/// The line for a `step.progress.recorded`: a question, a log line, an +/// agent envelope, a prompt's completion. +fn format_progress(ts: &str, view: PetriItem<'_>, label: &str, styles: &Styles) -> Option { + if let Some(parsed) = view.parsed() { + match parsed.get("kind").and_then(Value::as_str) { + Some("question") => { + let question = parsed.get("question")?; + let text = question.get("text").and_then(Value::as_str).unwrap_or(""); + let mut line = format!( + "{ts} {} {}: {}", + styles.yellow.apply_to("?"), + styles.bold.apply_to(label), + text, + ); + if let Some(options) = question.get("options").and_then(Value::as_array) { + let labels: Vec<&str> = options + .iter() + .filter_map(|option| option.get("label").and_then(Value::as_str)) + .collect(); + if !labels.is_empty() { + let _ = write!(line, " {}", styles.dim.apply_to(labels.join(" "))); + } + } + return Some(line); + } + Some("question_expired") => { + let default = parsed + .get("default") + .and_then(Value::as_str) + .map_or_else(String::new, |default| format!(" (default {default})")); + return Some(format!( + "{ts} {} question expired{}", + styles.dim.apply_to("\u{21b3}"), + styles.dim.apply_to(&default), + )); + } + _ => {} + } + } + if let Some((_, line)) = view.log_line() { + return Some(format!( + "{ts} {} {}", + styles.dim.apply_to("\u{2502}"), + styles.dim.apply_to(line), + )); + } + let custom = view.custom()?; + match custom.get("kind").and_then(Value::as_str)? { + "pebble" => format_envelope(ts, custom.get("event")?, label, styles), + "attractor.prompt.completed" => { + let response = custom.get("response").and_then(Value::as_str).unwrap_or(""); + let header = format!("{ts} {} {}", "\u{1f4ac}", styles.bold.apply_to(label)); + let body = indented(styles, response, " "); + Some(format!("{header}\n{body}\n")) + } + "attractor.checkout" => { + let repository = custom + .get("repository") + .and_then(Value::as_str) + .unwrap_or("?"); + let commit = custom.get("commit").and_then(Value::as_str).unwrap_or(""); + Some(format!( + "{ts} Checkout: {} {}", + repository, + styles.dim.apply_to(short_sha(commit)), + )) + } + _ => None, + } +} + +/// The line for a Pebble coding-agent envelope: the assistant's text, a +/// tool call's start and end. +fn format_envelope(ts: &str, envelope: &Value, label: &str, styles: &Styles) -> Option { + let event = envelope.get("event")?.as_object()?; + let (variant, fields) = event.iter().next()?; + match variant.as_str() { + "AssistantMessage" => { + let model = fields.get("model").and_then(Value::as_str).unwrap_or("?"); + let text = fields.get("text").and_then(Value::as_str).unwrap_or(""); + let header = format!( + "{ts} {} {} {}", + "\u{1f4ac}", + styles.bold.apply_to(label), + styles.dim.apply_to(format!("[{model}]")), + ); + let body = indented(styles, text, " "); + Some(format!("{header}\n{body}\n")) + } + "ToolCallStarted" => { + let tool = fields + .get("tool_name") + .and_then(Value::as_str) + .unwrap_or("?"); + Some(format!( + "{ts} {} {}", + styles.dim.apply_to("\u{2699}"), + styles.dim.apply_to(tool), + )) + } + "ToolCallCompleted" => { + let tool = fields + .get("tool_name") + .and_then(Value::as_str) + .unwrap_or("?"); + let is_error = fields + .get("is_error") + .and_then(Value::as_bool) + .unwrap_or(false); + let (glyph, style) = if is_error { + ("\u{2717}", &styles.red) + } else { + ("\u{2713}", &styles.green) + }; + Some(format!("{ts} {} {}", style.apply_to(glyph), tool)) + } + _ => None, + } +} + +/// The line for a platform record, by its kind. +fn format_platform_record(ts: &str, record: &Value, styles: &Styles) -> Option { + let kind = record.get("kind")?.as_str()?; + match kind { + "run.created" => { + let spec = record.get("spec"); + let name = record + .get("title") + .and_then(Value::as_str) + .or_else(|| spec?.pointer("/settings/workflow/name")?.as_str()) + .or_else(|| spec?.get("workflow_slug")?.as_str()) + .unwrap_or("Run"); + let run_id = spec + .and_then(|spec| spec.get("run_id")) + .and_then(Value::as_str) + .unwrap_or("?"); + let header = format!( + "{ts} {} {} {}", + styles.bold_cyan.apply_to("\u{25b6}"), + styles.bold.apply_to(name), + styles.dim.apply_to(run_id), + ); + match spec + .and_then(|spec| spec.pointer("/settings/run/goal")) + .and_then(Value::as_str) + { + Some(goal) if !goal.is_empty() => { + let body = indented(styles, goal, " "); + Some(format!("{header}\n{body}\n")) + } + _ => Some(header), + } + } + "run.lifecycle" => { + let transition = record.get("transition").and_then(Value::as_str)?; + let reason = record + .get("reason") + .and_then(Value::as_str) + .map_or_else(String::new, |reason| format!(": {reason}")); + Some(format!( + "{ts} {}", + styles + .dim + .apply_to(format!("\u{00b7} {transition}{reason}")) + )) + } + "run.notice" => { + let level = record + .get("level") + .and_then(Value::as_str) + .unwrap_or("info"); + let code = record.get("code").and_then(Value::as_str).unwrap_or(""); + let message = record.get("message").and_then(Value::as_str).unwrap_or(""); + let label = match level { + "warn" => styles.yellow.apply_to("Warning:").to_string(), + "error" => styles.bold_red.apply_to("Error:").to_string(), + _ => styles.bold.apply_to("Info:").to_string(), + }; + let code_suffix = if code.is_empty() { + String::new() + } else { + format!(" {}", styles.dim.apply_to(format!("[{code}]"))) + }; + Some(format!("{ts} {label} {message}{code_suffix}")) + } + "checkpoint" => { + let sha = record + .get("git_commit_sha") + .and_then(Value::as_str) + .map_or_else(|| "(no commit)".to_string(), short_sha); + Some(format!( + "{ts} {} {}", + styles.dim.apply_to("\u{2398} Checkpoint"), + styles.dim.apply_to(sha), + )) + } + "pull_request.created" => { + let url = record + .get("html_url") + .and_then(Value::as_str) + .unwrap_or("?"); + let draft = record + .get("draft") + .and_then(Value::as_bool) + .unwrap_or(false); + let suffix = if draft { " (draft)" } else { "" }; + Some(format!( + "{ts} Pull request: {}{}", + url, + styles.dim.apply_to(suffix) + )) + } + "interview.answered" => { + let principal = record.get("principal"); + let who = principal + .and_then(|principal| principal.get("login")) + .or_else(|| principal?.get("kind")) + .and_then(Value::as_str) + .unwrap_or("?"); + Some(format!( + "{ts} {} answered by {}", + styles.dim.apply_to("\u{21b3}"), + styles.dim.apply_to(who), + )) + } + "run.title" => { + let title = record.get("title").and_then(Value::as_str).unwrap_or(""); + Some(format!("{ts} Title: {title}")) + } + "run.branch" => { + let branch = record + .get("run_branch") + .and_then(Value::as_str) + .unwrap_or("?"); + let base = record + .get("base_sha") + .and_then(Value::as_str) + .map_or_else(String::new, |sha| format!(" from {}", short_sha(sha))); + Some(format!( + "{ts} Branch: {}{}", + branch, + styles.dim.apply_to(&base) + )) + } + "git.identity" => { + let identity = record.get("identity")?; + let name = identity.get("name").and_then(Value::as_str).unwrap_or("?"); + let email = identity.get("email").and_then(Value::as_str).unwrap_or("?"); + let source = identity + .get("source") + .and_then(Value::as_str) + .unwrap_or("?"); + Some(format!( + "{ts} Git identity: {name} <{email}> {}", + styles.dim.apply_to(source) + )) + } + other => Some(format!( + "{ts} {}", + styles.dim.apply_to(format!("\u{00b7} {other}")) + )), + } +} + +fn short_sha(sha: &str) -> String { + sha.chars().take(7).collect() +} + +fn indented(styles: &Styles, text: &str, indent: &str) -> String { + let wrap_width = Styles::terminal_width().saturating_sub(indent.len()); + styles + .render_markdown_width(text, wrap_width) + .lines() + .map(|line| format!("{indent}{line}")) + .collect::>() + .join("\n") +} + +fn millis(recorded_at: u64) -> DateTime { + Utc.timestamp_millis_opt(i64::try_from(recorded_at).unwrap_or(i64::MAX)) + .single() + .unwrap_or_default() +} + +/// `run events` for a Petri run: the stream, filtered by `--since` and +/// `--tail`, raw or pretty, then followed live when asked. +pub(crate) async fn print_events( + client: &server_client::Client, + run_id: &RunId, + args: &EventsArgs, + since: Option>, + pretty: bool, + styles: &Styles, +) -> Result<()> { + let items = client + .list_run_stream(run_id, 0) + .await + .context("Failed to list the run's stream")?; + let last_seq = items.last().map_or(0, |item| item.stream_seq); + let mut selected: Vec<&RunStreamItem> = items + .iter() + .filter(|item| since.is_none_or(|cutoff| PetriItem::new(item).recorded_at() >= cutoff)) + .collect(); + if let Some(tail) = args.tail { + let start = selected.len().saturating_sub(tail); + selected.drain(..start); + } + + let stdout = io::stdout(); + let mut out = stdout.lock(); + let mut state = PrettyState::default(); + for item in selected { + write_item(&mut out, item, pretty, styles, &mut state)?; + } + out.flush()?; + + if args.follow { + Box::pin(follow(client, run_id, last_seq, pretty, styles, state)).await?; + } + Ok(()) +} + +fn write_item( + out: &mut dyn Write, + item: &RunStreamItem, + pretty: bool, + styles: &Styles, + state: &mut PrettyState, +) -> Result<()> { + if pretty { + if let Some(line) = format_pretty(item, styles, state) { + writeln!(out, "{line}")?; + } + } else { + writeln!(out, "{}", raw_line(item)?)?; + } + Ok(()) +} + +/// Follow the stream live from `after`: attach, print each item, and on a +/// stream the server ended before the run's terminal record, reconnect +/// from the last `stream_seq` printed unless the run has concluded. +async fn follow( + client: &server_client::Client, + run_id: &RunId, + after: u64, + pretty: bool, + styles: &Styles, + mut state: PrettyState, +) -> Result<()> { + let stdout = io::stdout(); + let mut out = stdout.lock(); + let mut cursor = after; + loop { + let mut stream = client.attach_run_stream(run_id, Some(cursor)).await?; + while let Some(item) = stream.next_item().await? { + cursor = item.stream_seq; + write_item(&mut out, &item, pretty, styles, &mut state)?; + out.flush()?; + if is_terminal_lifecycle(&item) { + return Ok(()); + } + } + let run_state = client + .get_run_state(run_id) + .await + .context("Failed to read run state from server while following the stream")?; + if run_state.status.is_terminal() { + // The server ended the stream after its grace: print what + // landed since, if anything, and stop. + for item in client.list_run_stream(run_id, cursor).await? { + write_item(&mut out, &item, pretty, styles, &mut state)?; + } + out.flush()?; + debug!("Run reached terminal status and the stream ended, stopping follow"); + return Ok(()); + } + debug!( + cursor, + "the attached stream ended before the run did; reconnecting" + ); + time::sleep(FOLLOW_RECONNECT_DELAY).await; + } +} + +#[cfg(test)] +mod tests { + use fabro_types::fixtures; + use serde_json::json; + + use super::*; + + fn item(kind: RunStreamItemKind, stream_seq: u64, value: Value) -> RunStreamItem { + RunStreamItem { + run_id: fixtures::RUN_1, + stream_seq, + kind, + id: stream_seq.to_string(), + recorded_at: 1_789_706_579_000 + stream_seq * 1000, + item: value, + } + } + + fn petri(stream_seq: u64, value: Value) -> RunStreamItem { + item(RunStreamItemKind::Petri, stream_seq, value) + } + + fn platform(stream_seq: u64, record: &Value) -> RunStreamItem { + item( + RunStreamItemKind::Platform, + stream_seq, + json!({"seq": stream_seq, "recorded_at": 0, "record": record}), + ) + } + + fn subject(name: &str, kind: &str) -> Value { + json!({ + "node": {"id": 2, "name": name, "kind": "attractor/x", "meta": {"label": name, "kind": kind}}, + "firing": 2, "visit": 1, "attempt": 1, "generation": 0, "branch": {"role": "none"} + }) + } + + fn view_event(name: &str, node: &str, kind: &str, derived: Value) -> Value { + let mut derived = derived; + derived["event"] = json!(name); + json!({ + "id": {"log": "execution", "execution": 0, "seq": 5, "index": 1}, + "origin": "derived", + "context": {"invocation": 0, "execution": 0}, + "subject": subject(node, kind), + "derived": derived + }) + } + + #[test] + fn a_stage_renders_its_start_and_its_end_with_the_elapsed_time() { + let styles = Styles::new(false); + let mut state = PrettyState::default(); + let started = petri(1, view_event("visit.started", "say", "command", json!({}))); + let completed = petri( + 4, + view_event( + "visit.completed", + "say", + "command", + json!({ + "outcome": {"status": "success", "metrics": {"duration_ms": 42}}, + "executed": true, "attempts": 1 + }), + ), + ); + let start_line = format_pretty(&started, &styles, &mut state).expect("a start line"); + assert!(start_line.contains("\u{25b6} say"), "{start_line}"); + let end_line = format_pretty(&completed, &styles, &mut state).expect("an end line"); + assert!(end_line.contains("\u{2713} say"), "{end_line}"); + assert!( + end_line.contains(" 3s"), + "the visit took three seconds: {end_line}" + ); + } + + #[test] + fn a_fork_delegate_is_not_a_stage_but_its_branch_completion_is_shown() { + let styles = Styles::new(false); + let mut state = PrettyState::default(); + let delegate = petri( + 1, + view_event("visit.started", "a", "parallel.branch", json!({})), + ); + assert!(format_pretty(&delegate, &styles, &mut state).is_none()); + let completed = petri( + 3, + view_event( + "branch.completed", + "a", + "parallel.branch", + json!({ + "result": {"node": {"name": "a"}, "status": "success"} + }), + ), + ); + let line = format_pretty(&completed, &styles, &mut state).expect("a branch line"); + assert!(line.contains("branch a"), "{line}"); + assert!(line.contains(" 2s"), "{line}"); + } + + #[test] + fn a_platform_notice_and_the_terminal_lifecycle_record_render_by_kind() { + let styles = Styles::new(false); + let mut state = PrettyState::default(); + let notice = platform( + 2, + &json!({"kind": "run.notice", "level": "warn", "code": "x.y", "message": "careful"}), + ); + let line = format_pretty(¬ice, &styles, &mut state).expect("a notice line"); + assert!(line.contains("Warning: careful [x.y]"), "{line}"); + let finished = platform( + 3, + &json!({"kind": "run.lifecycle", "transition": "succeeded", "status": {"kind": "succeeded"}}), + ); + assert!(is_terminal_lifecycle(&finished)); + assert_eq!(exit_code_of(&finished), Some(0)); + let line = format_pretty(&finished, &styles, &mut state).expect("a lifecycle line"); + assert!(line.contains("\u{00b7} succeeded"), "{line}"); + } + + #[test] + fn a_question_is_found_and_closed_by_its_answer_or_by_who_answered() { + let asked = petri( + 5, + json!({ + "origin": "external", + "context": {"invocation": 0, "execution": 0}, + "subject": subject("gate", "human"), + "record": {"seq": 12, "body": {"event": "step.progress.recorded", "firing": 2, + "ev": {"custom": {"$question": {"id": "gate#2", "text": "Go?"}}}}}, + "derived": {"parsed": {"kind": "question", "question": {"id": "gate#2", "text": "Go?", + "options": [{"key": "Y", "label": "[Y] Yes"}], "kind": "yes_no"}}} + }), + ); + assert_eq!(question_of(&asked), Some(("gate#2", "Go?"))); + let styles = Styles::new(false); + let mut state = PrettyState::default(); + let line = format_pretty(&asked, &styles, &mut state).expect("a question line"); + assert!(line.contains("? gate: Go? [Y] Yes"), "{line}"); + + let answered = petri( + 6, + json!({ + "origin": "external", + "context": {"invocation": 0, "execution": 0}, + "subject": subject("gate", "human"), + "record": {"seq": 14, "body": {"event": "control.requested", "firing": 2, + "ctl": {"deliver": {"$answer": {"question": "gate#2", "choice": "N"}}}}}, + "derived": {"deliverable": true, "answer": {"question": "gate#2", "choice": "N"}} + }), + ); + assert!(resolves_question(&answered, "gate#2")); + assert!(!resolves_question(&answered, "gate#3")); + let who = platform( + 7, + &json!({"kind": "interview.answered", "question": "gate#2", "principal": {"kind": "user", "login": "dev"}}), + ); + assert!(resolves_question(&who, "gate#2")); + let line = format_pretty(&who, &styles, &mut state).expect("an answered-by line"); + assert!(line.contains("answered by dev"), "{line}"); + } + + #[test] + fn the_engine_finish_decides_the_exit_code() { + let finished = petri( + 9, + json!({ + "origin": "external", "context": {}, + "record": {"seq": 6, "body": {"event": "run.finished", "status": "failed"}} + }), + ); + assert_eq!(exit_code_of(&finished), Some(1)); + let styles = Styles::new(false); + let mut state = PrettyState::default(); + let line = format_pretty(&finished, &styles, &mut state).expect("a finish line"); + assert!(line.contains("\u{2717} FAILED"), "{line}"); + } + + #[test] + fn the_raw_line_is_the_envelope_as_json() { + let notice = platform( + 2, + &json!({"kind": "run.notice", "level": "info", "message": "m"}), + ); + let line = raw_line(¬ice).expect("a line"); + let value: Value = serde_json::from_str(&line).expect("json"); + assert_eq!(value["stream_seq"], 2); + assert_eq!(value["kind"], "platform"); + assert_eq!(value["item"]["record"]["kind"], "run.notice"); + } +} diff --git a/lib/apps/fabro-cli/src/commands/run/run_progress/event.rs b/lib/apps/fabro-cli/src/commands/run/run_progress/event.rs index e98535eac..93a7fc7cb 100644 --- a/lib/apps/fabro-cli/src/commands/run/run_progress/event.rs +++ b/lib/apps/fabro-cli/src/commands/run/run_progress/event.rs @@ -388,7 +388,23 @@ fn agent_progress_event( stored: &RunEvent, event: &CodingEvent, ) -> Option { - let root_session = stored.parent_session_id.is_none(); + coding_progress_event( + node_id, + stored.parent_session_id.is_none(), + Some(stored.ts), + event, + ) +} + +/// The progress line for one coding agent event, given whether it came +/// from the root session and when it was recorded: the mapping the legacy +/// envelope and a Petri stream envelope share. +pub(super) fn coding_progress_event( + node_id: String, + root_session: bool, + timestamp: Option>, + event: &CodingEvent, +) -> Option { match event { CodingEvent::AssistantMessage { model, .. } => Some(ProgressEvent::AssistantMessage { stage_node_id: node_id, @@ -401,10 +417,10 @@ fn agent_progress_event( arguments, } => Some(ProgressEvent::ToolCallStarted { stage_node_id: node_id, - tool_name: tool_name.clone(), - tool_call_id: tool_call_id.clone(), - arguments: arguments.clone(), - timestamp: Some(stored.ts), + tool_name: tool_name.clone(), + tool_call_id: tool_call_id.clone(), + arguments: arguments.clone(), + timestamp, }), CodingEvent::ToolCallCompleted { tool_call_id, @@ -412,10 +428,10 @@ fn agent_progress_event( .. } => Some(ProgressEvent::ToolCallCompleted { stage_node_id: node_id, - tool_call_id: tool_call_id.clone(), - is_error: *is_error, - duration_ms: None, - timestamp: Some(stored.ts), + tool_call_id: tool_call_id.clone(), + is_error: *is_error, + duration_ms: None, + timestamp, }), CodingEvent::Warning { kind, details, .. } if kind == "context_window" => { let usage_percent = details diff --git a/lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs b/lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs index 04b838b95..020ea4332 100644 --- a/lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs +++ b/lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs @@ -3,10 +3,11 @@ reason = "sync CLI run-progress renderer: writes to std::io::stderr directly" )] -use fabro_types::{RunEvent, RunNoticeCode}; +use fabro_types::{RunEvent, RunNoticeCode, RunStreamItem}; mod event; mod info_display; +mod petri; mod renderer; mod setup_display; mod stage_display; @@ -14,6 +15,7 @@ mod styles; use event::{ProgressEvent, from_json_line, from_run_event}; use info_display::InfoDisplay; +use petri::PetriProgressState; use renderer::ProgressRenderer; use setup_display::SetupDisplay; use stage_display::StageDisplay; @@ -24,6 +26,7 @@ pub(crate) struct ProgressUI { setup: SetupDisplay, info: InfoDisplay, saw_metadata_snapshot_failure: bool, + petri: PetriProgressState, } impl ProgressUI { @@ -46,6 +49,7 @@ impl ProgressUI { setup: SetupDisplay::new(verbose), info: InfoDisplay::new(verbose), saw_metadata_snapshot_failure: false, + petri: PetriProgressState::default(), } } @@ -95,6 +99,14 @@ impl ProgressUI { } } + /// One item of a Petri run's stream: the progress lines it means, if + /// any, rendered as a legacy event's would be. + pub(crate) fn handle_stream_item(&mut self, item: &RunStreamItem) { + for progress_event in petri::progress_events(item, &mut self.petri) { + self.dispatch(progress_event); + } + } + fn dispatch(&mut self, event: ProgressEvent) { let renderer = &self.renderer; match event { diff --git a/lib/apps/fabro-cli/src/commands/run/run_progress/petri.rs b/lib/apps/fabro-cli/src/commands/run/run_progress/petri.rs new file mode 100644 index 000000000..502b5c062 --- /dev/null +++ b/lib/apps/fabro-cli/src/commands/run/run_progress/petri.rs @@ -0,0 +1,264 @@ +//! The progress lines a Petri run's stream items mean: the mapping from +//! Petri's `.` events and Fabro's platform records onto the +//! [`ProgressEvent`]s the renderer already draws for a legacy run. +//! +//! A stage is a firing whose node is a logical stage (`VIEWS.md`); its +//! display key is the node's name, as a legacy stage's `node_id` is. A +//! fork's `parallel.branch` delegates are the branches of the parallel +//! group, never stages of their own. + +use fabro_types::run_event::RunNoticeLevel; +use fabro_types::{CodingAgentEvent, RunStreamItem, StageOutcome, StageTiming}; +use serde_json::Value; + +use super::event::{ProgressEvent, ProgressUsage, coding_progress_event}; +use crate::commands::run::petri_stream::{PetriItem, StageClock}; + +/// What the mapping remembers between items: when each firing started. +#[derive(Default)] +pub(super) struct PetriProgressState { + clock: StageClock, +} + +/// The progress events one stream item means, in order. +pub(super) fn progress_events( + item: &RunStreamItem, + state: &mut PetriProgressState, +) -> Vec { + let elapsed = state.clock.observe(item); + let view = PetriItem::new(item); + if let Some(record) = view.platform_record() { + return platform_progress_event(record).into_iter().collect(); + } + let Some(name) = view.name() else { + return Vec::new(); + }; + let node_id = view.node_name().unwrap_or("?").to_string(); + let label = view.node_label().unwrap_or("?").to_string(); + match name { + "visit.started" => { + if view.is_shown_stage() { + vec![ProgressEvent::StageStarted { + node_id, + name: label, + script: None, + }] + } else if view.node_kind() == Some("parallel.branch") { + vec![ProgressEvent::ParallelBranchStarted { branch: node_id }] + } else { + Vec::new() + } + } + "visit.completed" if view.is_shown_stage() => { + let Some(derived) = view.derived() else { + return Vec::new(); + }; + let outcome = derived.get("outcome"); + let executed = derived + .get("executed") + .and_then(Value::as_bool) + .unwrap_or(true); + let status = outcome + .and_then(|outcome| outcome.get("status")) + .and_then(Value::as_str) + .unwrap_or("?"); + let timing = StageTiming { + wall_time_ms: elapsed.unwrap_or(0), + ..StageTiming::default() + }; + let completed = + |status: &str, usage: Option| ProgressEvent::StageCompleted { + node_id: node_id.clone(), + name: label.clone(), + timing, + status: status.to_string(), + usage, + }; + if !executed || status == "skipped" { + return vec![completed("skipped", None)]; + } + match status { + "success" => vec![completed("succeeded", outcome.and_then(usage_of))], + "partial_success" => { + vec![completed("partially_succeeded", outcome.and_then(usage_of))] + } + "cancelled" => vec![completed("cancelled", None)], + other => { + let error = outcome + .and_then(|outcome| outcome.pointer("/failure/message")) + .and_then(Value::as_str) + .unwrap_or(other) + .to_string(); + vec![ProgressEvent::StageFailed { + node_id, + name: label, + error, + }] + } + } + } + "retry.scheduled" => { + let Some(derived) = view.derived() else { + return Vec::new(); + }; + let attempt = derived + .get("next_attempt") + .and_then(Value::as_u64) + .unwrap_or(0); + let delay_ms = derived + .pointer("/base_delay/secs") + .and_then(Value::as_u64) + .map(|secs| secs.saturating_mul(1000)) + .or_else(|| derived.get("base_delay").and_then(Value::as_u64)) + .unwrap_or(0); + vec![ProgressEvent::StageRetrying { + name: label, + attempt, + max_attempts: attempt, + delay_ms, + }] + } + "fork.started" => vec![ProgressEvent::ParallelStarted], + "branch.completed" => { + let Some(result) = view.derived().and_then(|derived| derived.get("result")) else { + return Vec::new(); + }; + let branch = result + .pointer("/node/name") + .and_then(Value::as_str) + .unwrap_or(&node_id) + .to_string(); + let status = match result.get("status").and_then(Value::as_str) { + Some("success") => StageOutcome::Succeeded, + Some("partial_success") => StageOutcome::PartiallySucceeded, + _ => StageOutcome::Failed { + retry_requested: false, + }, + }; + vec![ProgressEvent::ParallelBranchCompleted { + branch, + duration_ms: elapsed.unwrap_or(0), + status, + }] + } + "fork.completed" => vec![ProgressEvent::ParallelCompleted], + "route.applied" => { + let Some(derived) = view.derived() else { + return Vec::new(); + }; + let Some(to_node) = derived.pointer("/target/name").and_then(Value::as_str) else { + return Vec::new(); + }; + let back = derived + .get("back") + .and_then(Value::as_bool) + .unwrap_or(false); + if back { + vec![ProgressEvent::LoopRestart { + from_node: node_id, + to_node: to_node.to_string(), + }] + } else { + vec![ProgressEvent::EdgeSelected { + from_node: node_id, + to_node: to_node.to_string(), + label: None, + condition: derived + .get("transition") + .and_then(Value::as_str) + .filter(|transition| *transition != "Continue") + .map(str::to_lowercase), + }] + } + } + "step.progress.recorded" => envelope_progress_event(view, node_id).into_iter().collect(), + _ => Vec::new(), + } +} + +/// The progress line of a Pebble coding-agent envelope on a stage's +/// stream, if the terminal shows it. +fn envelope_progress_event(view: PetriItem<'_>, node_id: String) -> Option { + let custom = view.custom()?; + if custom.get("kind").and_then(Value::as_str) != Some("pebble") { + return None; + } + let envelope: CodingAgentEvent = serde_json::from_value(custom.get("event")?.clone()).ok()?; + let root_session = envelope.parent_session_id.is_none(); + coding_progress_event( + node_id, + root_session, + Some(view.recorded_at()), + &envelope.event, + ) +} + +/// The stage's model usage from a finished visit's metrics, when the step +/// reported one. +fn usage_of(outcome: &Value) -> Option { + let custom = outcome.pointer("/metrics/custom")?; + let usage = custom + .get("pebble.usage") + .or_else(|| custom.get("prompt.usage"))?; + let tokens = usage.get("tokens")?; + Some(ProgressUsage { + input_tokens: tokens.get("input").and_then(Value::as_u64).unwrap_or(0), + output_tokens: tokens.get("output").and_then(Value::as_u64).unwrap_or(0), + cost: usage + .pointer("/cost/usd_micros") + .and_then(Value::as_u64) + .map(|micros| micros as f64 / 1_000_000.0), + }) +} + +/// The progress line a platform record means, if the terminal shows it. +fn platform_progress_event(record: &Value) -> Option { + match record.get("kind")?.as_str()? { + "run.created" => Some(ProgressEvent::RunCreated { + web_url: record + .get("web_url") + .and_then(Value::as_str) + .map(str::to_string), + }), + "run.branch" => Some(ProgressEvent::WorkflowStarted { + worktree_dir: None, + base_branch: record + .get("run_branch") + .and_then(Value::as_str) + .map(str::to_string), + base_sha: record + .get("base_sha") + .and_then(Value::as_str) + .map(str::to_string), + }), + "run.notice" => Some(ProgressEvent::RunNotice { + level: match record.get("level").and_then(Value::as_str) { + Some("warn") => RunNoticeLevel::Warn, + Some("error") => RunNoticeLevel::Error, + _ => RunNoticeLevel::Info, + }, + code: record + .get("code") + .and_then(Value::as_str) + .unwrap_or_default() + .to_string(), + message: record + .get("message") + .and_then(Value::as_str) + .unwrap_or_default() + .to_string(), + }), + "pull_request.created" => Some(ProgressEvent::PullRequestCreated { + pr_url: record + .get("html_url") + .and_then(Value::as_str) + .unwrap_or_default() + .to_string(), + draft: record + .get("draft") + .and_then(Value::as_bool) + .unwrap_or(false), + }), + _ => None, + } +} diff --git a/lib/apps/fabro-cli/src/server_client.rs b/lib/apps/fabro-cli/src/server_client.rs index 0d20ac36d..d8ad0a9cc 100644 --- a/lib/apps/fabro-cli/src/server_client.rs +++ b/lib/apps/fabro-cli/src/server_client.rs @@ -7,7 +7,7 @@ use fabro_client::{ AuthEntry, AuthStore, Credential, OAuthSession, ServerTarget, TransportConnector, apply_bearer_token_auth, }; -pub(crate) use fabro_client::{Client, RunEventStream}; +pub(crate) use fabro_client::{Client, RunEventStream, RunStreamItemStream}; use fabro_config::Storage; use fabro_config::bind::Bind; pub(crate) use fabro_types::RunProjection; diff --git a/lib/apps/fabro-cli/tests/it/scenario/petri.rs b/lib/apps/fabro-cli/tests/it/scenario/petri.rs index e8f6aa337..22686c181 100644 --- a/lib/apps/fabro-cli/tests/it/scenario/petri.rs +++ b/lib/apps/fabro-cli/tests/it/scenario/petri.rs @@ -22,8 +22,9 @@ #![expect(clippy::print_stderr, reason = "a skipped test says why on its stderr")] use std::env; +use std::io::{Read as _, Write as _}; use std::path::{Path, PathBuf}; -use std::process::{Child, Command, Stdio}; +use std::process::{Child, Command, Output, Stdio}; use std::time::{Duration, Instant}; use fabro_client::ServerTarget; @@ -33,14 +34,14 @@ use fabro_petri::checkpoint::CheckpointKey; use fabro_petri::engine::{self, RunStatus}; use fabro_petri::petri::RunKey; use fabro_static::EnvVars; -use fabro_store::{EventEnvelope, PlatformRecord, PlatformRecordKind, PlatformRecordStore}; -use fabro_test::{apply_test_isolation, expect_reqwest_json, isolated_storage_dir, test_context}; -use fabro_types::{EventBody, RunId}; +use fabro_store::{PlatformRecord, PlatformRecordKind, PlatformRecordStore}; +use fabro_test::{ + apply_test_isolation, expect_reqwest_json, fabro_snapshot, isolated_storage_dir, test_context, +}; +use fabro_types::RunId; use crate::cmd::support::created_run_id; -use crate::support::{ - TEST_DEV_TOKEN, TEST_SESSION_SECRET, parse_event_envelopes, seed_dev_token_auth, -}; +use crate::support::{TEST_DEV_TOKEN, TEST_SESSION_SECRET, seed_dev_token_auth}; const HOST_PLUGIN: &str = "sandbox-driver-host"; const REQUIRE_ENV: &str = "FABRO_REQUIRE_SANDBOX_PLUGINS"; @@ -471,39 +472,93 @@ async fn wait_for_status(server: &RunningServer, run_id: &str, expected: &[&str] } } -async fn run_events(server: &RunningServer, run_id: &str) -> Vec { - parse_event_envelopes(&run_json(server, &format!("runs/{run_id}/events")).await) +/// The run's stream, as `GET /runs/{id}/events` serves a Petri run: every +/// item in `stream_seq` order, in the stream envelope. +async fn run_stream(server: &RunningServer, run_id: &str) -> Vec { + let mut items = Vec::new(); + let mut after = 0; + loop { + let page = run_json( + server, + &format!("runs/{run_id}/events?after={after}&limit=1000"), + ) + .await; + let data = page["data"] + .as_array() + .cloned() + .expect("the stream page has a data array"); + let Some(last) = data.last() else { + break; + }; + after = last["stream_seq"].as_u64().expect("a stream_seq"); + let has_more = page["meta"]["has_more"].as_bool().unwrap_or(false); + items.extend(data); + if !has_more { + break; + } + } + items } -/// The run's events once `terminal` is among them: the events list is a -/// projection that settles after the run's status does. -async fn settled_events( - server: &RunningServer, - run_id: &str, - terminal: &str, -) -> Vec { +/// What each stream item is, for an assertion: a Petri event by its +/// `.` name (`question` and `question_expired` for the parsed +/// progress payloads), a platform lifecycle record as +/// `lifecycle:`, another platform record by its kind. +fn stream_names(items: &[serde_json::Value]) -> Vec { + items + .iter() + .map(|line| { + let item = &line["item"]; + if line["kind"] == "platform" { + let record = &item["record"]; + return match record["kind"].as_str().unwrap_or("?") { + "run.lifecycle" => { + format!("lifecycle:{}", record["transition"].as_str().unwrap_or("?")) + } + kind => kind.to_string(), + }; + } + if let Some(kind) = item["derived"]["parsed"]["kind"].as_str() { + if matches!(kind, "question" | "question_expired") { + return kind.to_string(); + } + } + item["record"]["body"]["event"] + .as_str() + .or_else(|| item["derived"]["event"].as_str()) + .unwrap_or("?") + .to_string() + }) + .collect() +} + +fn count_of(names: &[String], expected: &str) -> usize { + names.iter().filter(|name| *name == expected).count() +} + +/// The run's whole stream once it is settled: Fabro's terminal lifecycle +/// record lands a moment after the engine's finish (the worker exits, the +/// server records the status, the projector folds it), so a reader that +/// wants the end of the stream waits for that record. +async fn settled_stream(server: &RunningServer, run_id: &str) -> Vec { let deadline = Instant::now() + RUN_TIMEOUT; loop { - let events = run_events(server, run_id).await; - if event_names(&events).contains(&terminal) { - return events; + let items = run_stream(server, run_id).await; + let names = stream_names(&items); + if names + .iter() + .any(|name| matches!(name.as_str(), "lifecycle:succeeded" | "lifecycle:failed")) + { + return items; } assert!( Instant::now() < deadline, - "`{terminal}` was never projected for run {run_id}: {:?}", - event_names(&events) + "run {run_id} never recorded its terminal lifecycle transition: {names:?}" ); tokio::time::sleep(POLL).await; } } -fn event_names(events: &[EventEnvelope]) -> Vec<&str> { - events - .iter() - .map(|envelope| envelope.event.event_name()) - .collect() -} - /// The pid of the worker subprocess the server launched for the run: the /// worker retitles itself `fabro `, so that /// is what the process table shows. @@ -572,18 +627,12 @@ async fn a_petri_run_executes_in_the_server_launched_worker() { let state = run_json(&server, &format!("runs/{run_id}/state")).await; assert_eq!(state["spec"]["engine"]["kind"], "petri", "state: {state}"); - let events = settled_events(&server, &run_id, "run.completed").await; - let names = event_names(&events); - assert_eq!( - names - .iter() - .filter(|name| **name == "run.completed") - .count(), - 1, - "{names:?}" - ); + let names = stream_names(&settled_stream(&server, &run_id).await); + assert_eq!(count_of(&names, "lifecycle:succeeded"), 1, "{names:?}"); + assert_eq!(count_of(&names, "run.finished"), 1, "{names:?}"); assert!( - names.contains(&"run.starting") && names.contains(&"run.running"), + names.iter().any(|name| name == "lifecycle:starting") + && names.iter().any(|name| name == "lifecycle:running"), "{names:?}" ); @@ -612,7 +661,7 @@ async fn a_petri_run_executes_in_the_server_launched_worker() { /// A Petri run whose worker and server both die mid-stage continues after /// the server restarts: the new server releases the dead worker's lease, /// asks the run to start again as a resume, and launches a worker in -/// resume mode, which finishes the run with one `run.completed`. +/// resume mode, which finishes the run with one terminal lifecycle record. #[tokio::test(flavor = "multi_thread")] async fn a_petri_run_resumes_in_a_new_worker_after_the_server_restarts() { if host_plugin().is_none() { @@ -659,40 +708,35 @@ async fn a_petri_run_resumes_in_a_new_worker_after_the_server_restarts() { std::fs::write(&gate, "go").expect("the gate opens"); let status = wait_for_status(&server, &run_id, &["succeeded", "failed"]).await; + let items = settled_stream(&server, &run_id).await; + let names = stream_names(&items); assert_eq!( status, "succeeded", - "server stderr:\n{}", + "stream: {names:?}\nserver stderr:\n{}", server.stderr_text() ); - let events = settled_events(&server, &run_id, "run.completed").await; - let names = event_names(&events); - assert_eq!( - names - .iter() - .filter(|name| **name == "run.completed") - .count(), - 1, - "{names:?}" - ); + assert_eq!(count_of(&names, "lifecycle:succeeded"), 1, "{names:?}"); + assert_eq!(count_of(&names, "run.finished"), 1, "{names:?}"); // `fabro run` asked for the first start; the restart asked for a // resume, after the run had been running. let first_running = names .iter() - .position(|name| *name == "run.running") + .position(|name| name == "lifecycle:running") .expect("the run ran before the crash"); - let resume_request = events + let resume_request = items .iter() - .position(|envelope| { - matches!( - &envelope.event.body, - EventBody::RunStartRequested(props) if props.resume - ) + .position(|line| { + let record = &line["item"]["record"]; + line["kind"] == "platform" + && record["kind"] == "run.lifecycle" + && record["transition"] == "start_requested" + && record["source"] == "resume" }) .expect("the restart asked for a resume"); assert!(resume_request > first_running, "{names:?}"); assert_eq!( - names.iter().filter(|name| **name == "run.running").count(), + count_of(&names, "lifecycle:running"), 2, "the run ran once before and once after the restart: {names:?}" ); @@ -846,11 +890,11 @@ async fn a_human_gate_in_the_worker_is_answered_through_the_api() { markers.join("no").exists() && !markers.join("yes").exists(), "the no branch ran" ); - let names = run_events(&server, &run_id).await; - let names = event_names(&names); + let names = stream_names(&run_stream(&server, &run_id).await); assert!( - names.contains(&"interview.started") && names.contains(&"interview.completed"), - "{names:?}" + names.iter().any(|name| name == "question") + && names.iter().any(|name| name == "interview.answered"), + "the question and who answered it are on the stream: {names:?}" ); assert!(questions(&server, &run_id).await.is_empty()); let store = server.petri_store().await; @@ -945,13 +989,315 @@ async fn an_unanswered_gate_in_the_worker_expires_with_its_default() { markers.join("no").exists() && !markers.join("yes").exists(), "the default ran" ); - let events = run_events(&server, &run_id).await; - let names = event_names(&events); - assert!(names.contains(&"interview.timeout"), "{names:?}"); + let names = stream_names(&run_stream(&server, &run_id).await); + assert!( + names.iter().any(|name| name == "question_expired"), + "{names:?}" + ); assert!(questions(&server, &run_id).await.is_empty()); server.shutdown(); } +/// A CLI command against the server, as `run_detached` seeds its auth. +fn cli(context: &fabro_test::TestContext, server: &RunningServer, args: &[&str]) -> Output { + let target = server.target(); + let output = context + .command() + .args(args) + .args(["--server", &target]) + .output() + .expect("the CLI command executes"); + assert!( + output.status.success(), + "`fabro {}` failed\nstdout:\n{}\nstderr:\n{}", + args.join(" "), + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + output +} + +fn ndjson(output: &Output) -> Vec { + String::from_utf8_lossy(&output.stdout) + .lines() + .filter(|line| !line.trim().is_empty()) + .map(|line| serde_json::from_str(line).expect("a JSON line")) + .collect() +} + +/// The `.` name of a Petri item, or the kind of a platform +/// record, from a raw stream line. +fn stream_line_name(line: &serde_json::Value) -> String { + let item = &line["item"]; + if line["kind"] == "platform" { + return format!( + "platform:{}", + item["record"]["kind"].as_str().unwrap_or("?") + ); + } + item["record"]["body"]["event"] + .as_str() + .or_else(|| item["derived"]["event"].as_str()) + .unwrap_or("?") + .to_string() +} + +/// The snapshot filters for `events --pretty` over a Petri run: clocks, +/// durations and the run id vary per run. +fn pretty_filters(context: &fabro_test::TestContext) -> Vec<(String, String)> { + let mut filters = context.filters(); + filters.push((r"\b\d{2}:\d{2}:\d{2}\b".to_string(), "[CLOCK]".to_string())); + filters.push(( + r"\b\d+(\.\d+)?(ms|s)\b".to_string(), + "[DURATION]".to_string(), + )); + filters.push(( + r"Checkpoint [0-9a-f]{7}".to_string(), + "Checkpoint [SHA]".to_string(), + )); + filters +} + +/// A finished Petri run reads back through the CLI: `events` prints the +/// stream envelope raw, dense in `stream_seq`; `events --pretty` renders +/// the stages by `.` with their labels and the platform +/// records by kind; `attach` replays it and exits with the run's status; +/// `wait` and `runs inspect` read the projection. +#[tokio::test(flavor = "multi_thread")] +async fn a_finished_petri_run_reads_back_through_the_cli() { + if host_plugin().is_none() { + return; + } + let context = test_context!(); + let server = RunningServer::start().await; + let workspace = write_petri_workspace(&context, "echo hello from petri"); + let run_id = run_detached(&context, &server, &workspace); + let status = wait_for_status(&server, &run_id, &["succeeded", "failed"]).await; + assert_eq!( + status, + "succeeded", + "server stderr:\n{}", + server.stderr_text() + ); + settled_stream(&server, &run_id).await; + let target = server.target(); + + // Raw: the envelope, one item per line, dense and in order. + let raw = cli(&context, &server, &["events", &run_id]); + let lines = ndjson(&raw); + let seqs: Vec = lines + .iter() + .map(|line| line["stream_seq"].as_u64().expect("a stream_seq")) + .collect(); + let expected: Vec = (1..=seqs.len() as u64).collect(); + assert_eq!(seqs, expected, "stream_seq is dense"); + for line in &lines { + assert_eq!(line["run_id"], run_id, "{line}"); + assert!( + line["id"].is_string() && line["recorded_at"].is_u64(), + "{line}" + ); + } + let names: Vec = lines.iter().map(stream_line_name).collect(); + for expected in [ + "platform:run.created", + "platform:run.lifecycle", + "run.started", + "visit.started", + "visit.completed", + "run.finished", + ] { + assert!( + names.iter().any(|name| name == expected), + "{expected} is on the stream: {names:?}" + ); + } + assert_eq!( + names.last().map(String::as_str), + Some("platform:run.lifecycle"), + "the terminal lifecycle record ends the stream: {names:?}" + ); + + // Tail: the last two items only. + let tail = cli(&context, &server, &["events", "--tail", "2", &run_id]); + assert_eq!(ndjson(&tail).len(), 2); + + // Pretty: stages and platform records. + let mut cmd = context.command(); + cmd.args(["events", "--pretty", "--server", &target, &run_id]); + fabro_snapshot!(pretty_filters(&context), cmd, @r" + success: true + exit_code: 0 + ----- stdout ----- + [CLOCK] ▶ Run one command [ULID] + [CLOCK] · submitted + [CLOCK] · start_requested + [CLOCK] · runnable + [CLOCK] · starting + [CLOCK] · running + [CLOCK] Engine: petri run started + [CLOCK] ▶ start + [CLOCK] │ checkout: [TEMP_DIR]/petri-workspace is not a Git repository; the workspace starts empty + [CLOCK] ✓ start [DURATION] + [CLOCK] ⎘ Checkpoint [SHA] + [CLOCK] ▶ say + [CLOCK] start → say continue + [CLOCK] │ hello from petri + [CLOCK] ✓ say [DURATION] + [CLOCK] ⎘ Checkpoint [SHA] + [CLOCK] ▶ exit + [CLOCK] say → exit continue + [CLOCK] ✓ exit [DURATION] + [CLOCK] ⎘ Checkpoint [SHA] + [CLOCK] ✓ SUCCEEDED [DURATION] + [CLOCK] · succeeded + ----- stderr ----- + "); + + // Attach replays the finished run and exits with its status. + let attach = cli(&context, &server, &["attach", &run_id]); + let stderr = String::from_utf8_lossy(&attach.stderr); + assert!(stderr.contains("say"), "the stage is drawn: {stderr}"); + + // Wait reads the projection's status and conclusion. + let wait = cli(&context, &server, &["wait", &run_id]); + let stderr = String::from_utf8_lossy(&wait.stderr); + assert!(stderr.contains("Succeeded"), "{stderr}"); + + // Inspect reads the projection, whose spec names the engine. + let inspect = cli(&context, &server, &["inspect", &run_id]); + let inspected: serde_json::Value = + serde_json::from_slice(&inspect.stdout).expect("inspect prints JSON"); + let entry = &inspected[0]; + assert_eq!(entry["run_id"], run_id, "{entry}"); + assert_eq!(entry["run_spec"]["engine"]["kind"], "petri", "{entry}"); + assert_eq!(entry["conclusion"]["status"], "succeeded", "{entry}"); + server.shutdown(); +} + +/// `attach` on a Petri run with a human gate asks the question at the +/// terminal and answers it through the questions API; the answer routes +/// the gate and the attach exits with the run's status. +#[tokio::test(flavor = "multi_thread")] +async fn attach_asks_a_petri_gate_at_the_terminal_and_answers_it() { + if host_plugin().is_none() { + return; + } + let context = test_context!(); + let server = RunningServer::start().await; + let markers = context.temp_dir.join("markers"); + std::fs::create_dir_all(&markers).expect("the marker dir creates"); + let workspace = write_petri_workflow(&context, &gate_dot(&markers, "")); + let run_id = run_detached_with(&context, &server, &workspace, &[]); + wait_for_questions(&server, &run_id, 1).await; + + let target = server.target(); + let mut attach_cmd = Command::new(env!("CARGO_BIN_EXE_fabro")); + apply_test_isolation(&mut attach_cmd, &context.home_dir); + attach_cmd + .current_dir(&context.temp_dir) + .args(["attach", "--server", &target, &run_id]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let mut child = attach_cmd.spawn().expect("attach spawns"); + { + let mut stdin = child.stdin.take().expect("attach stdin is piped"); + stdin.write_all(b"N\n").expect("the answer writes"); + } + let output = child + .wait_with_output() + .expect("attach exits once the run ends"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "attach failed\nstderr:\n{stderr}\nserver stderr:\n{}", + server.stderr_text() + ); + assert!(stderr.contains("Go?"), "the question was asked: {stderr}"); + assert!( + markers.join("no").exists() && !markers.join("yes").exists(), + "the no branch ran" + ); + let status = wait_for_status(&server, &run_id, &["succeeded", "failed"]).await; + assert_eq!(status, "succeeded"); + server.shutdown(); +} + +/// `events --follow` on a Petri run follows the stream live from its +/// cursor: the items already stored print first, the ones committed while +/// the run goes on follow, and the terminal lifecycle record ends it. +#[tokio::test(flavor = "multi_thread")] +async fn events_follow_streams_a_petri_run_live_to_its_end() { + if host_plugin().is_none() { + return; + } + let context = test_context!(); + let server = RunningServer::start().await; + let gate = context.temp_dir.join("go"); + let workspace = write_petri_workspace( + &context, + &format!( + "while [ ! -f {} ]; do sleep 0.05; done; echo released", + gate.display() + ), + ); + let run_id = run_detached(&context, &server, &workspace); + wait_for_status(&server, &run_id, &["running"]).await; + + let target = server.target(); + let mut follow_cmd = Command::new(env!("CARGO_BIN_EXE_fabro")); + apply_test_isolation(&mut follow_cmd, &context.home_dir); + follow_cmd + .current_dir(&context.temp_dir) + .args([ + "events", "--follow", "--pretty", "--server", &target, &run_id, + ]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let mut child = follow_cmd.spawn().expect("events --follow spawns"); + // Let the follower attach before the run is released. + tokio::time::sleep(Duration::from_millis(500)).await; + std::fs::write(&gate, b"").expect("the release marker writes"); + + let deadline = Instant::now() + RUN_TIMEOUT; + let status = loop { + if let Some(status) = child.try_wait().expect("the follower polls") { + break status; + } + assert!( + Instant::now() < deadline, + "events --follow did not end with the run; server stderr:\n{}", + server.stderr_text() + ); + tokio::time::sleep(POLL).await; + }; + let mut stdout = String::new(); + child + .stdout + .take() + .expect("stdout is piped") + .read_to_string(&mut stdout) + .expect("stdout reads"); + let mut stderr = String::new(); + child + .stderr + .take() + .expect("stderr is piped") + .read_to_string(&mut stderr) + .expect("stderr reads"); + assert!(status.success(), "events --follow failed: {stderr}"); + assert!(stdout.contains("▶ say"), "the stage started: {stdout}"); + assert!(stdout.contains("│ released"), "the live log line: {stdout}"); + assert!(stdout.contains("✓ SUCCEEDED"), "the finish: {stdout}"); + assert!( + stdout.trim_end().ends_with("· succeeded"), + "the terminal lifecycle record ends the follow: {stdout}" + ); + server.shutdown(); +} + /// A shell loop that waits for `gate` to exist. fn wait_for(gate: &Path) -> String { format!("while [ ! -f {} ]; do sleep 0.05; done", gate.display()) @@ -1277,12 +1623,18 @@ async fn a_failed_checkpoint_fails_the_run_and_a_restart_leaves_it_failed() { let status = wait_for_status(&server, &run_id, &["succeeded", "failed"]).await; let run = run_json(&server, &format!("runs/{run_id}")).await; assert_eq!(status, "failed", "run: {run}"); - let failures: Vec = settled_events(&server, &run_id, "run.failed") + // The run's failure travels on the stream as the platform record of + // its terminal lifecycle transition, with the failure's message as the + // reason. + let failures: Vec = settled_stream(&server, &run_id) .await .iter() - .filter_map(|envelope| match &envelope.event.body { - EventBody::RunFailed(props) => Some(props.failure.detail.message.clone()), - _ => None, + .filter_map(|line| { + let record = &line["item"]["record"]; + (line["kind"] == "platform" + && record["kind"] == "run.lifecycle" + && record["transition"] == "failed") + .then(|| record["reason"].as_str().unwrap_or_default().to_string()) }) .collect(); assert_eq!(failures.len(), 1, "{failures:?}"); diff --git a/lib/apps/fabro-server/src/server/handler/events.rs b/lib/apps/fabro-server/src/server/handler/events.rs index ec8fc93fa..6fe7b3ed7 100644 --- a/lib/apps/fabro-server/src/server/handler/events.rs +++ b/lib/apps/fabro-server/src/server/handler/events.rs @@ -1,12 +1,17 @@ use std::sync::Arc; +use std::time::Duration; use axum::extract::DefaultBodyLimit; +use fabro_api::types::PaginatedRunStreamList; +use fabro_petri::petri::EVENT_CONTRACT_VERSION; use fabro_types::run_event::MAX_RUN_EVENT_BODY_BYTES; use fabro_types::{ RunEventDetailContent, RunEventDetailContentKind, RunEventDetailEnvelope, - RunEventDetailResponse, + RunEventDetailResponse, RunStreamItem, }; use fabro_workflow::event::build_redacted_event_payload; +use tokio::sync::broadcast::error::RecvError; +use tokio::time::{self, Instant}; use super::super::{ ApiError, AppState, AppendEventResponse, BroadcastStream, Event, EventBody, EventEnvelope, @@ -70,9 +75,12 @@ struct RunEventListParams { #[serde(default)] before_seq: Option, #[serde(default)] - order: EventSequenceOrder, + order: Option, #[serde(default)] limit: Option, + /// The run stream cursor of a Petri run: the last `stream_seq` seen. + #[serde(default)] + after: Option, } impl RunEventListParams { @@ -80,12 +88,21 @@ impl RunEventListParams { self.since_seq.unwrap_or(1).max(1) } + fn order(&self) -> EventSequenceOrder { + self.order.unwrap_or_default() + } + fn limit(&self) -> usize { self.limit.unwrap_or(100).clamp(1, 1000) } fn cursor_error(&self) -> Option<&'static str> { - match self.order { + if self.after.is_some() && (self.since_seq.is_some() || self.before_seq.is_some()) { + return Some( + "after is the run stream cursor and cannot be combined with since_seq or before_seq.", + ); + } + match self.order() { EventSequenceOrder::Asc if self.before_seq.is_some() => { Some("before_seq requires order=desc.") } @@ -95,12 +112,27 @@ impl RunEventListParams { _ => None, } } + + /// Why the parameters do not address a Petri run's stream, if they do + /// not: the legacy cursors have no meaning there. + fn stream_cursor_error(&self) -> Option<&'static str> { + if self.since_seq.is_some() || self.before_seq.is_some() || self.order.is_some() { + return Some( + "this run executes on Petri; its events are a run stream addressed by `after` \ + (the last stream_seq seen), not by since_seq, before_seq or order.", + ); + } + None + } } #[derive(serde::Deserialize)] struct AttachParams { #[serde(default)] since_seq: Option, + /// The run stream cursor of a Petri run: the last `stream_seq` seen. + #[serde(default)] + after: Option, } #[derive(serde::Deserialize)] @@ -256,9 +288,25 @@ async fn list_run_events( } let limit = params.limit(); + match run_is_petri(&state, &id).await { + Ok(true) => { + if let Some(detail) = params.stream_cursor_error() { + return ApiError::bad_request(detail).into_response(); + } + return list_run_stream(&state, id, params.after.unwrap_or(0), limit).await; + } + Ok(false) => {} + Err(response) => return response, + } + if params.after.is_some() { + return ApiError::bad_request( + "after is the run stream cursor of a Petri run; this run's events use since_seq.", + ) + .into_response(); + } match state.stores.runs.open_run_reader(&id).await { Ok(run_store) => { - let events = match params.order { + let events = match params.order() { EventSequenceOrder::Asc => { run_store .list_events_from_with_limit(params.since_seq(), limit) @@ -291,6 +339,171 @@ async fn list_run_events( } } +/// Whether the run executes on Petri, from its stored spec; the canonical +/// 404 when there is no such run. +async fn run_is_petri(state: &AppState, id: &RunId) -> Result { + let projection = state + .load_run_projection(id) + .await + .map_err(IntoResponse::into_response)?; + Ok(projection.spec.engine.is_petri()) +} + +/// One page of a Petri run's stream past `after`. +async fn list_run_stream(state: &AppState, id: RunId, after: u64, limit: usize) -> Response { + match state + .petri_projector + .stream_after(id, after, limit.saturating_add(1)) + .await + { + Ok(mut items) => { + let has_more = items.len() > limit; + items.truncate(limit); + Json(PaginatedRunStreamList { + data: items, + meta: PaginationMeta { + has_more, + total: None, + }, + event_contract_version: EVENT_CONTRACT_VERSION, + }) + .into_response() + } + Err(err) => { + ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response() + } + } +} + +fn sse_event_from_stream_item(item: &RunStreamItem) -> Option { + let data = serde_json::to_string(item).ok()?; + let data = redact_jsonl_line(&data); + Some(Event::default().data(data)) +} + +/// How many stream items one read takes while attached. +const STREAM_ATTACH_BATCH_LIMIT: usize = 256; + +/// How long an attached reader waits for a commit signal before it re-reads +/// its cursor anyway: a signal is a wake-up, never the source of facts. +const STREAM_ATTACH_POLL: Duration = Duration::from_secs(1); + +/// How long an attached reader keeps following a run whose projection is +/// already terminal, waiting for the platform record of the terminal +/// lifecycle transition that ends the stream; after that it ends anyway. +const STREAM_ATTACH_TERMINAL_GRACE: Duration = Duration::from_secs(15); + +/// Whether the item ends an attached stream: the platform record of the +/// run's terminal lifecycle transition, which Fabro writes after the engine +/// recorded the run's finish. The analog of the legacy stream's +/// `run.completed` and `run.failed`. +fn stream_item_is_terminal(item: &RunStreamItem) -> bool { + if item.kind != fabro_types::RunStreamItemKind::Platform { + return false; + } + let record = &item.item["record"]; + record["kind"].as_str() == Some("run.lifecycle") + && matches!( + record["transition"].as_str(), + Some("succeeded" | "failed" | "dead") + ) +} + +/// The live stream of a Petri run from `after` (the last `stream_seq` the +/// client saw; `None` starts at the next unseen item), as server-sent +/// events. Every committed item past the cursor is sent once, in order, +/// and the stream ends once the run is no longer active and every +/// committed item is out. +async fn attach_run_stream(state: Arc, id: RunId, after: Option) -> Response { + let cursor = match after { + Some(after) => after, + None => match state.petri_projector.stream_head(id).await { + Ok(head) => head.unwrap_or(0), + Err(err) => { + return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) + .into_response(); + } + }, + }; + let (sender, receiver) = mpsc::unbounded_channel(); + let shutdown = state.shutdown_token(); + tokio::spawn(async move { + // Subscribed before the first read, so a pass that commits between + // the read and the wait is not missed. + let mut committed = state.petri_projector.subscribe(); + let mut cursor = cursor; + // Set once the projection is terminal: the stream then ends at the + // terminal lifecycle record, or when the grace runs out. + let mut terminal_deadline: Option = None; + loop { + // Drain everything committed past the cursor. + let mut drained = false; + while !drained { + let Ok(items) = state + .petri_projector + .stream_after(id, cursor, STREAM_ATTACH_BATCH_LIMIT) + .await + else { + return; + }; + drained = items.len() < STREAM_ATTACH_BATCH_LIMIT; + for item in items { + cursor = item.stream_seq; + let terminal = stream_item_is_terminal(&item); + if let Some(sse_event) = sse_event_from_stream_item(&item) { + if sender + .send(Ok::(sse_event)) + .is_err() + { + return; + } + } + if terminal { + return; + } + } + } + + // The run's status is read after the drain, so an item + // committed with the finish is already out. Once terminal, the + // stream keeps following for the terminal lifecycle record, + // which Fabro writes after the engine's finish, for a bounded + // time. + if terminal_deadline.is_none() { + let active = match state.stores.runs.load_run_projection(&id).await { + Ok(Some(projection)) => run_projection_is_active(&projection), + Ok(None) | Err(_) => false, + }; + if !active { + terminal_deadline = Some(Instant::now() + STREAM_ATTACH_TERMINAL_GRACE); + } + } + if terminal_deadline.is_some_and(|deadline| Instant::now() >= deadline) { + return; + } + + // Wait for the projector to commit more of this run, or poll. + loop { + tokio::select! { + biased; + () = shutdown.cancelled() => return, + signal = committed.recv() => match signal { + Ok(run_id) if run_id == id => break, + Ok(_) => {} + Err(RecvError::Lagged(_)) => break, + Err(RecvError::Closed) => return, + }, + () = time::sleep(STREAM_ATTACH_POLL) => break, + } + } + } + }); + + Sse::new(UnboundedReceiverStream::new(receiver)) + .keep_alive(KeepAlive::default()) + .into_response() +} + async fn list_run_stage_events( RequireRunStageScoped(id, stage_id): RequireRunStageScoped, State(state): State>, @@ -446,6 +659,11 @@ async fn attach_run_events( Ok(id) => id, Err(response) => return response, }; + match run_is_petri(&state, &id).await { + Ok(true) => return attach_run_stream(state, id, params.after).await, + Ok(false) => {} + Err(response) => return response, + } let Ok(run_store) = state.stores.runs.open_run_reader(&id).await else { return ApiError::not_found("Run not found.").into_response(); }; diff --git a/lib/apps/fabro-server/tests/it/scenario/mod.rs b/lib/apps/fabro-server/tests/it/scenario/mod.rs index a58db82af..15ce45a22 100644 --- a/lib/apps/fabro-server/tests/it/scenario/mod.rs +++ b/lib/apps/fabro-server/tests/it/scenario/mod.rs @@ -2,6 +2,7 @@ mod archive; mod dry_run; mod lifecycle; mod petri; +mod petri_stream; mod run_completion; mod sse; mod usage; diff --git a/lib/apps/fabro-server/tests/it/scenario/petri.rs b/lib/apps/fabro-server/tests/it/scenario/petri.rs index 3816fbde9..365755a23 100644 --- a/lib/apps/fabro-server/tests/it/scenario/petri.rs +++ b/lib/apps/fabro-server/tests/it/scenario/petri.rs @@ -105,14 +105,14 @@ const PARALLEL_DOT: &str = r#"digraph Parallel { merge -> exit }"#; -const PLAIN_SETTINGS: &str = "_version = 1\n\n[workflow]\ngraph = \"workflow.fabro\"\n"; +pub(super) const PLAIN_SETTINGS: &str = "_version = 1\n\n[workflow]\ngraph = \"workflow.fabro\"\n"; const PETRI_SETTINGS: &str = "_version = 1\n\n[workflow]\ngraph = \"workflow.fabro\"\nengine = \"petri\"\n"; /// The host plugin as Petri's lookup finds it: the override variable, else /// the executable on `PATH`. `None`, after saying so, when the test should /// skip; a panic when the environment forbids a skip. -fn host_plugin() -> Option { +pub(super) fn host_plugin() -> Option { let found = env::var_os(HOST_PLUGIN_OVERRIDE) .map(PathBuf::from) .or_else(|| { @@ -132,7 +132,7 @@ fn host_plugin() -> Option { /// Register a version whose entrypoint is `workflow.fabro`, with the given /// files beside it. -async fn register_version(app: &axum::Router, files: &[(&str, &str)]) -> String { +pub(super) async fn register_version(app: &axum::Router, files: &[(&str, &str)]) -> String { let entrypoint = WorkflowPath::new("workflow.fabro").expect("entrypoint path is valid"); let files = files .iter() @@ -150,7 +150,7 @@ async fn register_version(app: &axum::Router, files: &[(&str, &str)]) -> String .to_string() } -fn intent(version_id: &str, workspace: &std::path::Path) -> serde_json::Value { +pub(super) fn intent(version_id: &str, workspace: &std::path::Path) -> serde_json::Value { serde_json::json!({ "workflow_version_id": version_id, "target": {"kind": "folder", "path": workspace}, @@ -187,7 +187,11 @@ async fn petri_outcome(state: &AppState, run_id: &str) -> engine::RunOutcome { } /// The run's projected state once its projector settled. -async fn settled_state(state: &AppState, app: &axum::Router, run_id: &str) -> serde_json::Value { +pub(super) async fn settled_state( + state: &AppState, + app: &axum::Router, + run_id: &str, +) -> serde_json::Value { let id: RunId = run_id.parse().expect("the run id parses"); state.test_petri_projector().settle(id).await; let req = Request::builder() @@ -335,6 +339,7 @@ async fn the_hello_bundle_runs_on_petri_when_the_version_names_the_engine() { .any(|request| request["model"] == OPENAI_MODEL), "the prompt stage should have called the twin, got {logs}" ); + super::petri_stream::capture_settled(&state, &app, &run_id, "hello").await; } /// A command-only bundle runs on Petri when the server's setting names the @@ -379,6 +384,7 @@ async fn a_command_bundle_runs_on_petri_under_the_server_setting() { ); let stream = petri_stream_len(&state, &run_id).await; assert!(stream > 0, "the run's stream holds its events"); + super::petri_stream::capture_settled(&state, &app, &run_id, "command").await; } /// A parallel bundle with two command branches runs on Petri through the @@ -685,4 +691,5 @@ async fn a_human_gate_is_answered_through_the_questions_api() { record.principal.is_some(), "the answering principal: {record:?}" ); + super::petri_stream::capture_settled(&state, &app, &run_id, "gate").await; } diff --git a/lib/apps/fabro-server/tests/it/scenario/petri_stream.rs b/lib/apps/fabro-server/tests/it/scenario/petri_stream.rs new file mode 100644 index 000000000..3aab3d53b --- /dev/null +++ b/lib/apps/fabro-server/tests/it/scenario/petri_stream.rs @@ -0,0 +1,421 @@ +//! The run stream of a Petri run through the server: `GET /runs/{id}/events` +//! pages it by `after`, `GET /runs/{id}/attach` follows it live, and a +//! client that disconnects mid-run and reconnects from its last +//! `stream_seq` receives every item once, in order, with no gap and no +//! duplicate, including a platform record Fabro recorded between two +//! concurrent child executions' events. +//! +//! The runs execute in the server process under the handler-registry test +//! override and take their host scope through the sandbox-driver host +//! plugin, so the tests skip, and say why, when the executable is not +//! found (see `petri.rs`). +//! +//! With `FABRO_CAPTURE_PETRI_FIXTURES` set, a scenario also writes its +//! settled projection and full stream as JSON under the web app's test +//! fixtures (`apps/fabro-web/app/test-fixtures/petri/`), which the web +//! app's rendering tests read. + +#![expect( + clippy::disallowed_methods, + reason = "the tests locate the plugin executable and the capture switch through the process environment" +)] +#![expect(clippy::print_stderr, reason = "a skipped test says why on its stderr")] + +use std::collections::BTreeSet; +use std::env; +use std::sync::Arc; +use std::time::Duration; + +use axum::body::Body; +use axum::http::{Request, StatusCode}; +use fabro_server::server::AppState; +use fabro_store::platform_records::{PlatformRecord, PlatformRecordStore, RunNoticeRecord}; +use fabro_types::run_event::RunNoticeLevel; +use fabro_types::{RunId, RunStreamItem, RunStreamItemKind}; +use http_body_util::BodyExt; +use tokio::time::timeout; +use tower::ServiceExt; + +use super::petri::{PLAIN_SETTINGS, host_plugin, intent, register_version, settled_state}; +use crate::helpers::{ + api, create_and_start_run_from_intent, repo_root, response_json, run_json, settings_from_toml, + test_app_state_with_options, test_app_with_scheduler, wait_for_run_status, +}; + +const CAPTURE_ENV: &str = "FABRO_CAPTURE_PETRI_FIXTURES"; +const FRAME_TIMEOUT: Duration = Duration::from_secs(20); + +/// Two command branches that announce they started and wait for a release +/// marker, so a test can act between their events. +fn gated_parallel_dot(markers: &std::path::Path) -> String { + let dir = markers.display(); + format!( + r#"digraph Parallel {{ + graph [goal="Run two branches"] + start [shape=Mdiamond] + exit [shape=Msquare] + fork [shape=component] + a [shape=parallelogram, script="touch {dir}/a.started; while [ ! -f {dir}/go ]; do sleep 0.05; done; echo a"] + b [shape=parallelogram, script="touch {dir}/b.started; while [ ! -f {dir}/go ]; do sleep 0.05; done; echo b"] + merge [shape=tripleoctagon] + start -> fork + fork -> a + fork -> b + a -> merge + b -> merge + merge -> exit +}}"# + ) +} + +/// One page of the run's stream past `after`. +async fn stream_page( + app: &axum::Router, + run_id: &str, + after: u64, + limit: usize, +) -> serde_json::Value { + let req = Request::builder() + .method("GET") + .uri(api(&format!( + "/runs/{run_id}/events?after={after}&limit={limit}" + ))) + .body(Body::empty()) + .expect("events request should build"); + let response = app + .clone() + .oneshot(req) + .await + .expect("events request routes"); + response_json( + response, + StatusCode::OK, + format!("GET /api/v1/runs/{run_id}/events?after={after}&limit={limit}"), + ) + .await +} + +/// Every item of the run's stream, paged through the listing endpoint. +pub(super) async fn list_stream( + app: &axum::Router, + run_id: &str, + page_limit: usize, +) -> Vec { + let mut after = 0; + let mut items = Vec::new(); + loop { + let page = stream_page(app, run_id, after, page_limit).await; + let data: Vec = + serde_json::from_value(page["data"].clone()).expect("stream items decode"); + let has_more = page["meta"]["has_more"] + .as_bool() + .expect("has_more is a bool"); + assert_eq!( + page["event_contract_version"].as_u64(), + Some(3), + "the server reports Petri's contract version: {page}" + ); + let Some(last) = data.last() else { + assert!(!has_more, "an empty page is the last"); + break; + }; + after = last.stream_seq; + items.extend(data); + if !has_more { + break; + } + } + items +} + +/// An attached reader of the run's stream that stops reading when `until` +/// says so, as a client that lost its connection would: the frames it saw +/// so far come back. +struct Attached { + body: Body, + pending: String, +} + +impl Attached { + async fn open(app: &axum::Router, run_id: &str, after: u64) -> Self { + let req = Request::builder() + .method("GET") + .uri(api(&format!("/runs/{run_id}/attach?after={after}"))) + .body(Body::empty()) + .expect("attach request should build"); + let response = app + .clone() + .oneshot(req) + .await + .expect("attach request routes"); + assert_eq!(response.status(), StatusCode::OK); + assert!( + response + .headers() + .get("content-type") + .and_then(|value| value.to_str().ok()) + .is_some_and(|value| value.contains("text/event-stream")), + "an SSE response" + ); + Self { + body: response.into_body(), + pending: String::new(), + } + } + + /// The next item on the stream, or `None` once the server ended it. + async fn next(&mut self) -> Option { + loop { + if let Some(end) = self.pending.find("\n\n") { + let frame = self.pending[..end].to_string(); + self.pending.drain(..end + 2); + let data = frame + .lines() + .filter_map(|line| line.strip_prefix("data:")) + .map(str::trim) + .collect::>() + .join("\n"); + if data.is_empty() { + continue; + } + return Some(serde_json::from_str(&data).expect("a stream item frame decodes")); + } + let frame = timeout(FRAME_TIMEOUT, self.body.frame()) + .await + .expect("the attached stream keeps sending or ends"); + match frame { + Some(Ok(frame)) => { + if let Some(data) = frame.data_ref() { + self.pending.push_str(&String::from_utf8_lossy(data)); + } + } + Some(Err(err)) => panic!("the attached stream failed: {err}"), + None => return None, + } + } + } +} + +fn petri_name(item: &RunStreamItem) -> Option<&str> { + (item.kind == RunStreamItemKind::Petri) + .then(|| item.name()) + .flatten() +} + +/// The subject's node name, for a node that is a stage of its own: the +/// `parallel.branch` delegate the fork's execution holds for each branch +/// shares the branch's name and is not one. +fn subject_node(item: &RunStreamItem) -> Option<&str> { + let node = &item.item["subject"]["node"]; + if node["meta"]["kind"].as_str() == Some("parallel.branch") { + return None; + } + node["name"].as_str() +} + +fn wait_for_marker(path: &std::path::Path) { + let deadline = std::time::Instant::now() + Duration::from_secs(20); + while !path.exists() { + assert!( + std::time::Instant::now() < deadline, + "{} never appeared", + path.display() + ); + std::thread::sleep(Duration::from_millis(20)); + } +} + +/// A client attached to a two-branch parallel run disconnects once both +/// branches have started, Fabro records a platform notice while they run, +/// the client reconnects from its last `stream_seq`, and the union of what +/// it saw is the whole stream: every item once, in `stream_seq` order, no +/// gap, no duplicate, with the notice between the branches' events. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn a_reconnecting_client_receives_every_stream_item_once_in_order() { + if host_plugin().is_none() { + return; + } + let workspace = tempfile::tempdir().expect("workspace tempdir"); + let markers = tempfile::tempdir().expect("marker tempdir"); + let settings = settings_from_toml( + "_version = 1\n\n[run.environment]\nid = \"local\"\n\n[server.execution]\nengine = \ + \"petri\"\n", + ); + let state = test_app_state_with_options(settings, 5); + let app = test_app_with_scheduler(Arc::clone(&state)); + + let dot = gated_parallel_dot(markers.path()); + let version_id = register_version(&app, &[ + ("workflow.fabro", &dot), + ("workflow.toml", PLAIN_SETTINGS), + ]) + .await; + let run_id = + create_and_start_run_from_intent(&app, intent(&version_id, workspace.path())).await; + let id: RunId = run_id.parse().expect("the run id parses"); + + // First connection: from the start of the run until both branches + // have a `visit.started` on the stream, then drop it. + let mut first = Attached::open(&app, &run_id, 0).await; + let mut seen_first: Vec = Vec::new(); + let mut started: BTreeSet = BTreeSet::new(); + while started.len() < 2 { + let item = first + .next() + .await + .expect("the stream runs until both branches started"); + if petri_name(&item) == Some("visit.started") { + if let Some(node @ ("a" | "b")) = subject_node(&item) { + started.insert(node.to_string()); + } + } + seen_first.push(item); + } + let last_seen = seen_first + .last() + .map(|item| item.stream_seq) + .expect("something was seen"); + drop(first); + + // Both branch scripts are running: record a platform fact between + // their events, as a checkpoint or a notice would be, then let them go. + wait_for_marker(&markers.path().join("a.started")); + wait_for_marker(&markers.path().join("b.started")); + let platform = PlatformRecordStore::new(state.test_petri_view_pool()); + let notice = platform + .append( + &id, + &PlatformRecord::RunNotice(RunNoticeRecord { + level: RunNoticeLevel::Info, + code: "test.between_branches".to_string(), + message: "recorded while both branches ran".to_string(), + }), + None, + ) + .await + .expect("the notice appends"); + state.test_petri_projector().signal(id); + state.test_petri_projector().settle(id).await; + std::fs::write(markers.path().join("go"), b"").expect("the release marker writes"); + + // Second connection: resume from the last stream_seq seen and read to + // the end of the stream, which the server closes once the run is done. + let mut second = Attached::open(&app, &run_id, last_seen).await; + let mut seen_second: Vec = Vec::new(); + while let Some(item) = second.next().await { + seen_second.push(item); + } + let status = wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await; + let run = run_json(&app, &run_id).await; + assert_eq!(status, "succeeded", "run: {run}"); + let projection = settled_state(&state, &app, &run_id).await; + assert_eq!(projection["status"]["kind"], "succeeded", "{projection}"); + + // The union is the whole stream, once each, in order, with no gap. + let mut union = seen_first; + union.extend(seen_second); + let seqs: Vec = union.iter().map(|item| item.stream_seq).collect(); + let expected: Vec = (1..=seqs.len() as u64).collect(); + assert_eq!( + seqs, expected, + "stream_seq is dense and strictly increasing" + ); + let ids: BTreeSet<&str> = union.iter().map(|item| item.id.as_str()).collect(); + assert_eq!(ids.len(), union.len(), "every item identity appears once"); + for item in &union { + assert_eq!(item.run_id, id); + assert!(item.recorded_at > 0, "{item:?}"); + } + + // The same stream, paged through the listing endpoint with small + // pages, is item for item what the attached client saw. + let listed = list_stream(&app, &run_id, 7).await; + assert_eq!(listed, union, "the listing pages the same stream"); + + // The notice sits between the branches' events. + let notice_seq = union + .iter() + .find(|item| item.kind == RunStreamItemKind::Platform && item.id == notice.seq.to_string()) + .map(|item| item.stream_seq) + .expect("the notice is on the stream"); + let branch_seqs = |name: &str| -> Vec { + union + .iter() + .filter(|item| { + petri_name(item) == Some(name) && matches!(subject_node(item), Some("a" | "b")) + }) + .map(|item| item.stream_seq) + .collect() + }; + let starts = branch_seqs("visit.started"); + let ends = branch_seqs("visit.completed"); + assert_eq!(starts.len(), 2, "{starts:?}"); + assert_eq!(ends.len(), 2, "{ends:?}"); + assert!( + starts.iter().all(|seq| *seq < notice_seq) && ends.iter().all(|seq| *seq > notice_seq), + "the notice ({notice_seq}) is between the branch starts {starts:?} and ends {ends:?}" + ); + let finished = union + .iter() + .filter(|item| petri_name(item) == Some("run.finished")) + .count(); + assert_eq!(finished, 1, "the stream ends with the run's finish"); + + // The legacy cursors are refused for a Petri run; the stream cursor is + // refused for nothing else. + let req = Request::builder() + .method("GET") + .uri(api(&format!("/runs/{run_id}/events?since_seq=1"))) + .body(Body::empty()) + .expect("events request should build"); + let response = app + .clone() + .oneshot(req) + .await + .expect("events request routes"); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + + capture_fixture(&app, &run_id, "parallel", &projection).await; +} + +/// Write the run's settled projection and its whole stream under the web +/// app's test fixtures, when the capture switch is set. +pub(super) async fn capture_fixture( + app: &axum::Router, + run_id: &str, + name: &str, + projection: &serde_json::Value, +) { + if env::var_os(CAPTURE_ENV).is_none() { + return; + } + let stream = list_stream(app, run_id, 1000).await; + let fixture = serde_json::json!({ + "run_id": run_id, + "projection": projection, + "stream": stream, + }); + let dir = repo_root().join("apps/fabro-web/app/test-fixtures/petri"); + std::fs::create_dir_all(&dir).expect("the fixture directory creates"); + let path = dir.join(format!("{name}.json")); + std::fs::write( + &path, + serde_json::to_string_pretty(&fixture).expect("the fixture serializes"), + ) + .expect("the fixture writes"); + eprintln!("captured {}", path.display()); +} + +/// Helpers the other Petri scenarios use to capture their fixtures. +pub(super) async fn capture_settled( + state: &AppState, + app: &axum::Router, + run_id: &str, + name: &str, +) { + if env::var_os(CAPTURE_ENV).is_none() { + return; + } + let projection = settled_state(state, app, run_id).await; + capture_fixture(app, run_id, name, &projection).await; +} diff --git a/lib/components/fabro-petri/README.md b/lib/components/fabro-petri/README.md index 451686873..d613eccab 100644 --- a/lib/components/fabro-petri/README.md +++ b/lib/components/fabro-petri/README.md @@ -87,7 +87,14 @@ Every adapter the integration plan describes lands here. every Petri run, at startup. A run that executes in the server process goes through `Projector::observe_store`, which signals after each append. A torn tail (a record Petri cannot read) holds the view where it stands - and reports the run incomplete with the reason. + and reports the run incomplete with the reason. The projector also serves + the stream back (`Projector::stream_after`, one `RunStreamItem` per row: + `run_id`, `stream_seq`, `kind`, the item's own `id`, `recorded_at`, the + item) and signals its readers after each committed pass + (`Projector::subscribe`), which is how `GET /runs/{id}/events` pages a + Petri run by `after` and `GET /runs/{id}/attach` follows it live. The + version of Petri's event contract the stream carries is + `petri::EVENT_CONTRACT_VERSION`. - The platform adapters the plan adds after it: hooks and the run tools. ### What the projection leaves default @@ -181,13 +188,25 @@ serving the projection over Petri's records; a human gate is answered through the questions API; and Petri's diagnostics refuse a run at create. The server's `petri_runs` unit tests cover the lease ending at worker exit and the restart reconcile that relaunches a worker in resume mode. +`lib/apps/fabro-server/tests/it/scenario/petri_stream.rs` covers the stream: +a client attached to a two-branch parallel run disconnects once both +branches started, a platform notice is recorded while both branch scripts +run, the client reconnects from its last `stream_seq`, and the union of +what it saw is the whole stream, every item once, in order, with the notice +between the branch events and the same as the paged listing. With +`FABRO_CAPTURE_PETRI_FIXTURES` set, the scenarios write their settled +projection and stream under `apps/fabro-web/app/test-fixtures/petri/`, +which the web app's rendering tests read. The worker path is covered with the real binary in `lib/apps/fabro-cli/tests/it/scenario/petri.rs`: a command-only Petri run executes in the worker a foreground server launched, its records reach `petri_records` over the HTTP store and its lease ends with the worker; and a run whose server and worker are both killed mid-stage resumes in a new -worker after the server restarts, with one `run.completed`; a human gate in -the worker is answered through the questions API over the control channel; -two parallel gates each bind their own answer; and an unanswered gate -expires with its default. +worker after the server restarts, with one terminal lifecycle record; a +human gate in the worker is answered through the questions API over the +control channel; two parallel gates each bind their own answer; and an +unanswered gate expires with its default. The same file reads a finished +run back through the CLI (`events` raw, tail and `--pretty`, `attach`, +`wait`, `inspect`), answers a gate from an attached terminal, and follows +a run live with `events --follow` to its end. diff --git a/lib/components/fabro-petri/src/petri.rs b/lib/components/fabro-petri/src/petri.rs index 726edac63..e4464cca0 100644 --- a/lib/components/fabro-petri/src/petri.rs +++ b/lib/components/fabro-petri/src/petri.rs @@ -3,6 +3,11 @@ //! depending on the Petri packages themselves. Only this crate names them in //! its `Cargo.toml`. +use petri_execution::events; pub use petri_store::{ Access, Digest, ExecutionId, LogId, OwnerId, Record, RunKey, RunLogs, RunStore, StoreError, }; + +/// The version of Petri's public event contract this build serves on the +/// run stream: every `petri` item of `GET /runs/{id}/events` follows it. +pub const EVENT_CONTRACT_VERSION: u32 = events::EVENT_CONTRACT_VERSION; diff --git a/lib/components/fabro-petri/src/projector.rs b/lib/components/fabro-petri/src/projector.rs index 3215a842b..2e304e9b2 100644 --- a/lib/components/fabro-petri/src/projector.rs +++ b/lib/components/fabro-petri/src/projector.rs @@ -46,13 +46,13 @@ use std::time::Duration; use fabro_db::DbPool; use fabro_store::platform_records::{PlatformRecordStore, StoredPlatformRecord, now_ms}; use fabro_store::{RunProjection, RunSummaryStore}; -use fabro_types::RunId; +use fabro_types::{RunId, RunStreamItem, RunStreamItemKind}; use fabro_util::error::collect_chain; use petri_execution::events::{self, EventId, EventSource, RunEvent}; use petri_execution::{Access, RunKey, RunStore as _, inspect}; use petri_store::StoreError; use serde::{Deserialize, Serialize}; -use tokio::sync::Mutex as AsyncMutex; +use tokio::sync::{Mutex as AsyncMutex, broadcast}; use tokio::time; use tracing::{debug, info, warn}; @@ -147,17 +147,21 @@ struct Slot { /// the server both are the one database; a test may hand it the run /// summary store's own pool for the views. pub struct Projector { - records: DbPool, - pool: DbPool, - store: SqliteRunStore, - platform: PlatformRecordStore, - slots: Mutex>, + records: DbPool, + pool: DbPool, + store: SqliteRunStore, + platform: PlatformRecordStore, + slots: Mutex>, /// One pass at a time per run: a signalled pass and the startup pass /// over the same run never interleave their reads and writes. - passes: Mutex>>>, + passes: Mutex>>>, /// Test-only: stop the next pass after its reads, before its view /// transaction, as a crash there would. - fault: AtomicBool, + fault: AtomicBool, + /// Sent after each committed pass that wrote stream rows: the run whose + /// stream grew. A wake-up for the stream's readers, never a source of + /// facts; a reader that lags re-reads from its cursor. + committed: broadcast::Sender, } impl std::fmt::Debug for Projector { @@ -180,9 +184,42 @@ impl Projector { slots: Mutex::default(), passes: Mutex::default(), fault: AtomicBool::new(false), + committed: broadcast::channel(COMMIT_SIGNAL_CAPACITY).0, }) } + /// A receiver that learns which run's stream grew after each committed + /// pass. A receiver that falls behind gets `Lagged` and treats it as a + /// wake-up for every run it follows. + #[must_use] + pub fn subscribe(&self) -> broadcast::Receiver { + self.committed.subscribe() + } + + /// The run's stream past the cursor: up to `limit` items with + /// `stream_seq > after`, in `stream_seq` order, each in Fabro's + /// envelope. `after = 0` reads from the first item. + pub async fn stream_after( + &self, + run_id: RunId, + after: u64, + limit: usize, + ) -> Result, ProjectError> { + stream_after(&self.pool, run_id, after, limit).await + } + + /// The last delivery sequence the run's view holds, or `None` when no + /// pass has committed a view for it. + pub async fn stream_head(&self, run_id: RunId) -> Result, ProjectError> { + let head: Option = + sqlx::query_scalar("SELECT stream_seq FROM petri_projection WHERE run_id = ?") + .bind(run_id.to_string()) + .fetch_optional(&self.pool) + .await + .map_err(ProjectError::Database)?; + Ok(head.map(|head| u64::try_from(head).unwrap_or(0))) + } + /// Schedule a pass for the run. A pass already running for it runs once /// more when it ends; any number of signals in between coalesce. pub fn signal(self: &Arc, run_id: RunId) { @@ -471,6 +508,10 @@ impl Projector { stream_seq, "Petri projection pass committed" ); + if !rows.is_empty() { + // No receiver is not an error: nobody follows the stream. + let _ = self.committed.send(run_id); + } Ok(PassReport { run_id, skipped: false, @@ -658,6 +699,52 @@ struct StreamRow { event_json: String, } +/// How many commit signals a slow reader may fall behind before it is told +/// it lagged and re-reads from its cursor. +const COMMIT_SIGNAL_CAPACITY: usize = 1024; + +/// The run's stream past the cursor, read from the view tables: up to +/// `limit` rows with `stream_seq > after`, in order, in Fabro's envelope. +pub async fn stream_after( + views: &DbPool, + run_id: RunId, + after: u64, + limit: usize, +) -> Result, ProjectError> { + let rows: Vec<(i64, String, String, String)> = sqlx::query_as( + "SELECT stream_seq, item_kind, item_id, event_json FROM petri_stream WHERE run_id = ? AND \ + stream_seq > ? ORDER BY stream_seq LIMIT ?", + ) + .bind(run_id.to_string()) + .bind(column(after)) + .bind(i64::try_from(limit).unwrap_or(i64::MAX)) + .fetch_all(views) + .await + .map_err(ProjectError::Database)?; + rows.into_iter() + .map(|(stream_seq, item_kind, item_id, event_json)| { + let item: serde_json::Value = + serde_json::from_str(&event_json).map_err(ProjectError::Encode)?; + let kind = match item_kind.as_str() { + "platform" => RunStreamItemKind::Platform, + _ => RunStreamItemKind::Petri, + }; + let recorded_at = item + .get("recorded_at") + .and_then(serde_json::Value::as_u64) + .unwrap_or(0); + Ok(RunStreamItem { + run_id, + stream_seq: u64::try_from(stream_seq).unwrap_or(0), + kind, + id: item_id, + recorded_at, + item, + }) + }) + .collect() +} + /// A Petri event id as the stream names it: `//`. #[must_use] pub fn event_id_text(id: &EventId) -> String { diff --git a/lib/foundation/fabro-api/build.rs b/lib/foundation/fabro-api/build.rs index 1834074df..1580c2020 100644 --- a/lib/foundation/fabro-api/build.rs +++ b/lib/foundation/fabro-api/build.rs @@ -658,6 +658,11 @@ fn main() { &[], ), ("EventEnvelope", "fabro_types::EventEnvelope", &[]), + ("RunStreamItem", "fabro_types::RunStreamItem", &[]), + ("RunStreamItemKind", "fabro_types::RunStreamItemKind", &[]), + ("RunEngine", "fabro_types::RunEngine", &[]), + ("PetriAdmission", "fabro_types::PetriAdmission", &[]), + ("PetriGraphRef", "fabro_types::PetriGraphRef", &[]), ("PullRequest", "fabro_types::PullRequest", &[]), ("PullRequestLink", "fabro_types::PullRequestLink", &[]), ( diff --git a/lib/foundation/fabro-api/src/lib.rs b/lib/foundation/fabro-api/src/lib.rs index 7fa81a11e..0fa5c0abf 100644 --- a/lib/foundation/fabro-api/src/lib.rs +++ b/lib/foundation/fabro-api/src/lib.rs @@ -52,25 +52,25 @@ pub mod types { ModelTestMode, ModelUsage, PairId, PairMessageId, PairMessageRecord, PairMessageRequest, PairRecord, PairStartRequest, PairStatus, PairTarget, PairTranscriptEntry, PairTranscriptResponse, ParallelBranchId, ParallelBranchResult, PendingInterviewRecord, - PermissionLevel, Principal, Provider, PullRequest, PullRequestCreation, - PullRequestCreationId, PullRequestCreationStatus, PullRequestDetails, + PermissionLevel, PetriAdmission, PetriGraphRef, Principal, Provider, PullRequest, + PullRequestCreation, PullRequestCreationId, PullRequestCreationStatus, PullRequestDetails, PullRequestDetailsStatus, PullRequestDetailsUnavailableReason, PullRequestLink, PullRequestMeta, PullRequestResponse, QuestionType, RepositoryRef, ReviewTarget, - ReviewTargetKind, Run, RunApproval, RunApprovalState, RunClientProvenance, RunEvent, - RunEventDetailContentKind, RunEventDetailResponse, RunFailure, RunIntent, RunIntentArgs, - RunPairStatusResponse, RunProjection, RunProvenance, RunRunnableSource, RunSandbox, - RunSandboxFailure, RunSandboxInstance, RunSandboxKind, RunSandboxPlan, RunSandboxRuntime, - RunServerProvenance, RunSessionMetadata, RunSize, RunTarget, SandboxDetails, SandboxInfo, - SandboxListMeta, SandboxListResponse, SandboxProviderKind, SandboxProviderLookupError, - SandboxService, SandboxServiceListResponse, SecretMetadata, SecretType, ServerSettings, - SessionDetail, SessionId, SessionStatus, SessionSummary, SessionTurn, - SkillActivationSource, SkillSummary, StageCompletion, StageContextWindow, - StageContextWindowUnavailableReason, StageHandler, StageId, StageInferenceProjection, - StageModelUsage, StageOutcome, StageProjection, StageState, StageToolBatchProjection, - SystemActorKind, SystemIntegrationStatus, SystemIntegrationsResponse, TodoListProjection, - ToolCategory, ToolSource, ToolSummary, TurnId, UpdateVariableRequest, UserPrincipal, - Variable, VariableListResponse, WorkflowPath, WorkflowSettings, WorkflowVersion, - WorkflowVersionId, + ReviewTargetKind, Run, RunApproval, RunApprovalState, RunClientProvenance, RunEngine, + RunEvent, RunEventDetailContentKind, RunEventDetailResponse, RunFailure, RunIntent, + RunIntentArgs, RunPairStatusResponse, RunProjection, RunProvenance, RunRunnableSource, + RunSandbox, RunSandboxFailure, RunSandboxInstance, RunSandboxKind, RunSandboxPlan, + RunSandboxRuntime, RunServerProvenance, RunSessionMetadata, RunSize, RunStreamItem, + RunStreamItemKind, RunTarget, SandboxDetails, SandboxInfo, SandboxListMeta, + SandboxListResponse, SandboxProviderKind, SandboxProviderLookupError, SandboxService, + SandboxServiceListResponse, SecretMetadata, SecretType, ServerSettings, SessionDetail, + SessionId, SessionStatus, SessionSummary, SessionTurn, SkillActivationSource, SkillSummary, + StageCompletion, StageContextWindow, StageContextWindowUnavailableReason, StageHandler, + StageId, StageInferenceProjection, StageModelUsage, StageOutcome, StageProjection, + StageState, StageToolBatchProjection, SystemActorKind, SystemIntegrationStatus, + SystemIntegrationsResponse, TodoListProjection, ToolCategory, ToolSource, ToolSummary, + TurnId, UpdateVariableRequest, UserPrincipal, Variable, VariableListResponse, WorkflowPath, + WorkflowSettings, WorkflowVersion, WorkflowVersionId, }; pub use lithos_llm::catalog::{ModelHandle, ProviderId}; pub use lithos_llm::types::{ diff --git a/lib/foundation/fabro-api/tests/run_engine_round_trip.rs b/lib/foundation/fabro-api/tests/run_engine_round_trip.rs new file mode 100644 index 000000000..914ae56cf --- /dev/null +++ b/lib/foundation/fabro-api/tests/run_engine_round_trip.rs @@ -0,0 +1,57 @@ +use std::any::{TypeId, type_name}; + +use fabro_api::types::{ + PetriAdmission as ApiPetriAdmission, PetriGraphRef as ApiPetriGraphRef, + RunEngine as ApiRunEngine, +}; +use fabro_types::{PetriAdmission, PetriGraphRef, RunEngine}; +use serde_json::json; + +#[test] +fn run_engine_reuses_canonical_types() { + assert_same_type::(); + assert_same_type::(); + assert_same_type::(); +} + +#[test] +fn the_legacy_engine_round_trips_as_its_kind_alone() { + let value = json!({ "kind": "legacy" }); + let engine: RunEngine = serde_json::from_value(value.clone()).unwrap(); + assert!(engine.is_legacy()); + assert_eq!(serde_json::to_value(&engine).unwrap(), value); +} + +#[test] +fn the_petri_engine_round_trips_with_its_admission_flattened() { + let value = json!({ + "kind": "petri", + "graph": { + "blob": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", + "digest": "sha256:root" + }, + "children": [ + { + "blob": "3cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", + "digest": "sha256:child" + } + ] + }); + let engine: RunEngine = serde_json::from_value(value.clone()).unwrap(); + let admission = engine + .petri() + .expect("a Petri engine carries its admission"); + assert_eq!(admission.graph.digest, "sha256:root"); + assert_eq!(admission.children.len(), 1); + assert_eq!(serde_json::to_value(&engine).unwrap(), value); +} + +fn assert_same_type() { + assert_eq!( + TypeId::of::(), + TypeId::of::(), + "{} should be the same type as {}", + type_name::(), + type_name::() + ); +} diff --git a/lib/foundation/fabro-api/tests/run_stream_item_round_trip.rs b/lib/foundation/fabro-api/tests/run_stream_item_round_trip.rs new file mode 100644 index 000000000..3147c9646 --- /dev/null +++ b/lib/foundation/fabro-api/tests/run_stream_item_round_trip.rs @@ -0,0 +1,74 @@ +use std::any::{TypeId, type_name}; + +use fabro_api::types::{ + RunStreamItem as ApiRunStreamItem, RunStreamItemKind as ApiRunStreamItemKind, +}; +use fabro_types::{RunStreamItem, RunStreamItemKind, fixtures}; +use serde_json::json; + +#[test] +fn run_stream_item_reuses_canonical_types() { + assert_same_type::(); + assert_same_type::(); +} + +#[test] +fn a_petri_item_round_trips_with_its_event_unchanged() { + let value = json!({ + "run_id": fixtures::RUN_1.to_string(), + "stream_seq": 12, + "kind": "petri", + "id": "execution 1/23/0", + "recorded_at": 1_789_323_217_459_u64, + "item": { + "id": { "log": "execution", "execution": 1, "seq": 23, "index": 0 }, + "origin": "core", + "recorded_at": 1_789_323_217_459_u64, + "context": { "invocation": 0, "execution": 1 }, + "subject": { + "node": { "id": 4, "name": "review", "kind": "attractor/agent", "meta": { "kind": "agent" } }, + "firing": 3, "visit": 1, "attempt": 1, "generation": 0, "branch": { "role": "none" } + }, + "record": { + "seq": 23, "origin": "core", "recorded_at": 1_789_323_217_459_u64, + "body": { "event": "route.applied", "kind": "jump", "firing": 3, "target": 7 } + }, + "derived": { "target": { "id": 7, "name": "finalize", "kind": "attractor/command", "meta": { "kind": "command" } } } + } + }); + let item: RunStreamItem = serde_json::from_value(value.clone()).unwrap(); + assert_eq!(item.kind, RunStreamItemKind::Petri); + assert_eq!(item.name(), Some("route.applied")); + assert_eq!(serde_json::to_value(&item).unwrap(), value); +} + +#[test] +fn a_platform_item_round_trips_with_its_record_unchanged() { + let value = json!({ + "run_id": fixtures::RUN_1.to_string(), + "stream_seq": 13, + "kind": "platform", + "id": "4", + "recorded_at": 1_789_323_217_500_u64, + "item": { + "seq": 4, + "recorded_at": 1_789_323_217_500_u64, + "record": { "kind": "checkpoint", "execution": 1, "firing": 3, "git_commit_sha": "abc123" }, + "position": { "execution": 1, "firing": 3 } + } + }); + let item: RunStreamItem = serde_json::from_value(value.clone()).unwrap(); + assert_eq!(item.kind, RunStreamItemKind::Platform); + assert_eq!(item.name(), Some("checkpoint")); + assert_eq!(serde_json::to_value(&item).unwrap(), value); +} + +fn assert_same_type() { + assert_eq!( + TypeId::of::(), + TypeId::of::(), + "{} should be the same type as {}", + type_name::(), + type_name::() + ); +} diff --git a/lib/foundation/fabro-client/src/client.rs b/lib/foundation/fabro-client/src/client.rs index c4bd9a22f..dc08556ec 100644 --- a/lib/foundation/fabro-client/src/client.rs +++ b/lib/foundation/fabro-client/src/client.rs @@ -15,7 +15,7 @@ use fabro_types::{ ArtifactUpload, BlobHash, EventEnvelope, Model, ModelTestMode, PairId, PairMessageRecord, PairMessageRequest, PairRecord, PairStartRequest, PairTranscriptResponse, Run, RunEvent, RunEventDetailResponse, RunId, RunPairStatusResponse, RunProjection, RunSessionMetadata, - SessionId, StageId, WorkflowVersion, WorkflowVersionId, + RunStreamItem, SessionId, StageId, WorkflowVersion, WorkflowVersionId, }; use fabro_util::exit::{ErrorExt, ExitClass}; use futures::future::BoxFuture; @@ -56,6 +56,24 @@ pub struct RunEventStream { buffered_events: VecDeque, } +/// The live stream of a Petri run, as `GET /runs/{id}/attach` serves it: +/// one `RunStreamItem` per `data:` frame, in `stream_seq` order. +pub struct RunStreamItemStream { + stream: progenitor_client::ByteStream, + pending_bytes: Vec, + buffered_items: VecDeque, +} + +/// One page of a Petri run's stream. +#[derive(Debug, Clone)] +pub struct RunStreamPage { + pub items: Vec, + pub has_more: bool, + /// Petri's `EVENT_CONTRACT_VERSION` the server serves; `None` when the + /// page was empty and the server reported no version beside it. + pub event_contract_version: Option, +} + type HttpByteStream = Pin> + Send>>; pub struct SessionEventStream { @@ -186,6 +204,42 @@ impl RunEventStream { } } +impl RunStreamItemStream { + #[must_use] + pub fn new(stream: progenitor_client::ByteStream) -> Self { + Self { + stream, + pending_bytes: Vec::new(), + buffered_items: VecDeque::new(), + } + } + + pub async fn next_item(&mut self) -> Result> { + loop { + if let Some(item) = self.buffered_items.pop_front() { + return Ok(Some(item)); + } + + if let Some(chunk) = self.stream.next().await { + let chunk = chunk.map_err(anyhow::Error::new)?; + self.pending_bytes.extend_from_slice(&chunk); + self.buffer_sse_items(false)?; + } else { + self.buffer_sse_items(true)?; + return Ok(self.buffered_items.pop_front()); + } + } + } + + fn buffer_sse_items(&mut self, finalize: bool) -> Result<()> { + for payload in sse::drain_sse_payloads(&mut self.pending_bytes, finalize) { + self.buffered_items + .push_back(serde_json::from_str(&payload)?); + } + Ok(()) + } +} + impl SessionEventStream { #[must_use] pub fn new(stream: HttpByteStream) -> Self { @@ -1813,7 +1867,15 @@ impl Client { request.send().await }) .await?; - let parsed = response.into_inner(); + let parsed = match response.into_inner() { + types::ListRunEventsResponse::EventList(page) => page, + types::ListRunEventsResponse::RunStreamList(_) => { + bail!( + "run {run_id} executes on Petri; its events are served as a run stream \ + (list_run_stream)" + ); + } + }; let events = parsed .data .into_iter() @@ -1822,6 +1884,84 @@ impl Client { Ok((events, parsed.meta.has_more)) } + /// One page of a Petri run's stream: up to `limit` items with + /// `stream_seq > after`, in order. + pub async fn list_run_stream_page( + &self, + run_id: &RunId, + after: u64, + limit: Option, + ) -> Result { + let response = self + .send_api(|client| async move { + let mut request = client.list_run_events().id(run_id.to_string()).after(after); + let page_limit = limit.map(|limit| limit.min(1000)); + if let Some(limit) = page_limit.and_then(non_zero_u64_from_usize) { + request = request.limit(limit); + } + request.send().await + }) + .await?; + match response.into_inner() { + types::ListRunEventsResponse::RunStreamList(page) => Ok(RunStreamPage { + items: page.data, + has_more: page.meta.has_more, + event_contract_version: Some(page.event_contract_version), + }), + // An empty page decodes as either list; a legacy page with + // items is a run that does not execute on Petri. + types::ListRunEventsResponse::EventList(page) if page.data.is_empty() => { + Ok(RunStreamPage { + items: Vec::new(), + has_more: page.meta.has_more, + event_contract_version: None, + }) + } + types::ListRunEventsResponse::EventList(_) => { + bail!("run {run_id} executes on the legacy engine; its events are not a run stream") + } + } + } + + /// Every item of a Petri run's stream past `after`, page by page. + pub async fn list_run_stream(&self, run_id: &RunId, after: u64) -> Result> { + let mut cursor = after; + let mut all = Vec::new(); + loop { + let page = self.list_run_stream_page(run_id, cursor, None).await?; + let Some(last) = page.items.last() else { + break; + }; + cursor = last.stream_seq; + let has_more = page.has_more; + all.extend(page.items); + if !has_more { + break; + } + } + Ok(all) + } + + /// The live stream of a Petri run from `after` (the last `stream_seq` + /// seen; `Some(0)` replays the whole run; `None` starts at the next + /// unseen item). + pub async fn attach_run_stream( + &self, + run_id: &RunId, + after: Option, + ) -> Result { + let response = self + .send_api(|client| async move { + let mut request = client.attach_run_events().id(run_id.to_string()); + if let Some(after) = after { + request = request.after(after); + } + request.send().await + }) + .await?; + Ok(RunStreamItemStream::new(response.into_inner())) + } + pub async fn attach_run_events( &self, run_id: &RunId, diff --git a/lib/foundation/fabro-client/src/lib.rs b/lib/foundation/fabro-client/src/lib.rs index e65e02560..d85fb6c47 100644 --- a/lib/foundation/fabro-client/src/lib.rs +++ b/lib/foundation/fabro-client/src/lib.rs @@ -12,7 +12,8 @@ pub use auth_store::{ AuthEntry, AuthStore, AuthStoreError, DevTokenEntry, LockError, OAuthEntry, StoredSubject, }; pub use client::{ - Client, RunEventStream, SessionEventStream, TransportConnector, apply_bearer_token_auth, + Client, RunEventStream, RunStreamItemStream, RunStreamPage, SessionEventStream, + TransportConnector, apply_bearer_token_auth, }; pub use credential::{Credential, CredentialFallback}; pub use error::{ diff --git a/lib/foundation/fabro-types/src/lib.rs b/lib/foundation/fabro-types/src/lib.rs index be945029a..543c02c62 100644 --- a/lib/foundation/fabro-types/src/lib.rs +++ b/lib/foundation/fabro-types/src/lib.rs @@ -35,6 +35,7 @@ pub mod run_id; pub mod run_intent; pub mod run_projection; pub mod run_sandbox; +pub mod run_stream; pub mod run_summary; pub mod run_title; pub mod sandbox_details; @@ -152,6 +153,7 @@ pub use run_sandbox::{ RunSandbox, RunSandboxFailure, RunSandboxInstance, RunSandboxKind, RunSandboxPlan, RunSandboxRuntime, }; +pub use run_stream::{RunStreamItem, RunStreamItemKind, petri_event_name}; pub use run_summary::{ AskFabro, AskFabroUnavailableReason, AutomationRef, ResolvedAutomationGitWorkflowSource, Run, RunApproval, RunApprovalState, RunError, RunLifecycle, RunLinks, RunModel, RunOrigin, diff --git a/lib/foundation/fabro-types/src/run_stream.rs b/lib/foundation/fabro-types/src/run_stream.rs new file mode 100644 index 000000000..dee4fa7b5 --- /dev/null +++ b/lib/foundation/fabro-types/src/run_stream.rs @@ -0,0 +1,168 @@ +//! The run stream: one ordered delivery of a Petri run's public events and +//! Fabro's platform records, as `GET /runs/{id}/events` and the attach +//! stream serve them for a run that executes on Petri. +//! +//! Each item is a Petri `RunEvent` (Petri's event contract, passed through +//! as JSON) or a stored platform record (Fabro's own fact about the run: +//! its lifecycle before and after the engine, a checkpoint with its commit, +//! a pull request), in one Fabro envelope. The envelope carries: +//! +//! - `stream_seq`, the durable per-run delivery sequence the projector assigned +//! when the item's record was committed. It is the cursor: a client resumes +//! from the last `stream_seq` it saw. It is dense and strictly increasing +//! within a run. +//! - `id`, the item's own identity, kept beside the cursor so a client +//! deduplicates by it: for a Petri event the `EventId` as +//! `//` (`coordinator/3/0`, `execution 1/23/0`), for a +//! platform record its `seq`. Petri's `EventId` is per log and has no +//! platform variant, so it is never the cursor. +//! - `kind`, which of the two the item is. +//! - `recorded_at`, when the item's record was appended, in milliseconds since +//! the Unix epoch; the same field both item shapes carry. +//! - `item`, the Petri `RunEvent` or the stored platform record, unchanged. + +use serde::{Deserialize, Serialize}; + +use crate::RunId; + +/// Which of the two item shapes a stream item carries. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + strum::Display, + strum::EnumString, + strum::IntoStaticStr, +)] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +pub enum RunStreamItemKind { + /// A Petri `RunEvent`: `{id, origin, recorded_at, context, subject, + /// record, derived}` under Petri's event contract. + Petri, + /// A stored platform record: `{seq, recorded_at, record: {kind, ...}, + /// position?}`. + Platform, +} + +/// One item of a Petri run's stream, in Fabro's envelope. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RunStreamItem { + pub run_id: RunId, + /// The delivery sequence: the cursor. + pub stream_seq: u64, + pub kind: RunStreamItemKind, + /// The item's own identity, for deduplication. + pub id: String, + /// Milliseconds since the Unix epoch when the item's record was + /// appended. + pub recorded_at: u64, + /// The Petri `RunEvent` or the stored platform record, as JSON. + pub item: serde_json::Value, +} + +impl RunStreamItem { + /// The `.` name of a Petri event, or the `kind` of a + /// platform record: what a listing shows and a filter matches on. + #[must_use] + pub fn name(&self) -> Option<&str> { + match self.kind { + RunStreamItemKind::Petri => petri_event_name(&self.item), + RunStreamItemKind::Platform => self + .item + .get("record") + .and_then(|record| record.get("kind")) + .and_then(serde_json::Value::as_str), + } + } +} + +/// The `.` name of a Petri `RunEvent` value: the recorded +/// body's `event` tag, or a view event's tag under `derived`. +#[must_use] +pub fn petri_event_name(event: &serde_json::Value) -> Option<&str> { + event + .get("record") + .and_then(|record| record.get("body")) + .and_then(|body| body.get("event")) + .and_then(serde_json::Value::as_str) + .or_else(|| { + event + .get("derived") + .and_then(|derived| derived.get("event")) + .and_then(serde_json::Value::as_str) + }) +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::{RunStreamItem, RunStreamItemKind, petri_event_name}; + use crate::fixtures; + + #[test] + fn kind_names_are_lowercase_in_both_directions() { + assert_eq!(RunStreamItemKind::Petri.to_string(), "petri"); + assert_eq!( + "platform".parse::(), + Ok(RunStreamItemKind::Platform) + ); + assert_eq!( + serde_json::to_value(RunStreamItemKind::Platform).expect("kind serializes"), + json!("platform") + ); + } + + #[test] + fn a_petri_item_is_named_by_its_recorded_event_tag() { + let item = RunStreamItem { + run_id: fixtures::RUN_1, + stream_seq: 4, + kind: RunStreamItemKind::Petri, + id: "coordinator/3/0".to_string(), + recorded_at: 1_789_323_217_366, + item: json!({ + "id": {"log": "coordinator", "seq": 3, "index": 0}, + "record": {"seq": 3, "body": {"event": "execution.declared"}} + }), + }; + assert_eq!(item.name(), Some("execution.declared")); + assert_eq!( + petri_event_name(&json!({"origin": "derived", "derived": {"event": "visit.started"}})), + Some("visit.started") + ); + } + + #[test] + fn a_platform_item_is_named_by_its_record_kind() { + let item = RunStreamItem { + run_id: fixtures::RUN_1, + stream_seq: 5, + kind: RunStreamItemKind::Platform, + id: "2".to_string(), + recorded_at: 1_789_323_217_400, + item: json!({"seq": 2, "record": {"kind": "run.notice", "level": "info"}}), + }; + assert_eq!(item.name(), Some("run.notice")); + } + + #[test] + fn the_envelope_round_trips_as_json() { + let value = json!({ + "run_id": fixtures::RUN_1.to_string(), + "stream_seq": 7, + "kind": "platform", + "id": "3", + "recorded_at": 1_789_323_217_400_u64, + "item": {"seq": 3, "recorded_at": 1_789_323_217_400_u64, "record": {"kind": "checkpoint", "execution": 1, "firing": 2}} + }); + let item: RunStreamItem = serde_json::from_value(value.clone()).expect("item decodes"); + assert_eq!(serde_json::to_value(&item).expect("item encodes"), value); + } +} diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index da688b54b..893cb3ea1 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -216,6 +216,7 @@ models/interview-option.ts models/interview-provider-settings.ts models/interview-question-record.ts models/link-run-pull-request-request.ts +models/list-run-events200-response.ts models/llm-output-kind.ts models/llm-retry-classification-after.ts models/llm-retry-classification-never.ts @@ -271,6 +272,7 @@ models/paginated-run-commit-list.ts models/paginated-run-file-list.ts models/paginated-run-list.ts models/paginated-run-stage-list.ts +models/paginated-run-stream-list.ts models/paginated-saved-query-list.ts models/paginated-session-list.ts models/paginated-workflow-list-response.ts @@ -296,7 +298,9 @@ models/pending-interview-record.ts models/pending-reason.ts models/permission-level.ts models/petri-access.ts +models/petri-admission.ts models/petri-append-request.ts +models/petri-graph-ref.ts models/petri-open-request.ts models/petri-open-response.ts models/petri-platform-record-append-request.ts @@ -386,6 +390,9 @@ models/run-commit.ts models/run-commits-meta.ts models/run-control-action.ts models/run-diff.ts +models/run-engine-one-of.ts +models/run-engine-one-of1.ts +models/run-engine.ts models/run-environment-settings.ts models/run-error.ts models/run-event-detail-response-content.ts @@ -445,6 +452,8 @@ models/run-status-starting.ts models/run-status-submitted.ts models/run-status-succeeded.ts models/run-status.ts +models/run-stream-item-kind.ts +models/run-stream-item.ts models/run-superseded-by-props.ts models/run-target.ts models/run-timestamps.ts diff --git a/lib/packages/fabro-api-client/src/api/run-internals-api.ts b/lib/packages/fabro-api-client/src/api/run-internals-api.ts index 46f924e9e..f9afb1097 100644 --- a/lib/packages/fabro-api-client/src/api/run-internals-api.ts +++ b/lib/packages/fabro-api-client/src/api/run-internals-api.ts @@ -30,6 +30,8 @@ import type { CommandLogResponse } from '../models'; // @ts-ignore import type { ErrorResponse } from '../models'; // @ts-ignore +import type { ListRunEvents200Response } from '../models'; +// @ts-ignore import type { PaginatedEventList } from '../models'; // @ts-ignore import type { PaginatedRunStageList } from '../models'; @@ -212,14 +214,15 @@ export const RunInternalsApiAxiosParamCreator = function (configuration?: Config }; }, /** - * Opens an ordered server-sent event stream starting at `since_seq`, replaying persisted events and continuing with live updates while the run remains active. + * Opens an ordered server-sent event stream, replaying persisted items and continuing with live updates while the run remains active. Each `data:` frame is one JSON object in the run engine\'s envelope. For a legacy run the frames are `EventEnvelope`s and the stream starts at `since_seq` (inclusive; the next unseen event when omitted). It ends after `run.completed` or `run.failed`. For a Petri run the frames are `RunStreamItem`s and the stream starts after `after` (the last `stream_seq` the client saw; `0` replays the whole run; the next unseen item when omitted). It ends once the run is no longer active and every committed item has been sent. A reconnecting client passes its last `stream_seq` as `after` and deduplicates by `id`. * @summary Attach Run Events * @param {string} id Unique run identifier (ULID). * @param {number} [sinceSeq] First event sequence number to include. + * @param {number} [after] Run stream cursor for a Petri run: the last `stream_seq` the client saw, exclusive. `0` starts at the first item. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - attachRunEvents: async (id: string, sinceSeq?: number, options: RawAxiosRequestConfig = {}): Promise => { + attachRunEvents: async (id: string, sinceSeq?: number, after?: number, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined assertParamExists('attachRunEvents', 'id', id) const localVarPath = `/api/v1/runs/{id}/attach` @@ -245,6 +248,10 @@ export const RunInternalsApiAxiosParamCreator = function (configuration?: Config localVarQueryParameter['since_seq'] = sinceSeq; } + if (after !== undefined) { + localVarQueryParameter['after'] = after; + } + localVarHeaderParameter['Accept'] = 'text/event-stream,application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); @@ -711,17 +718,18 @@ export const RunInternalsApiAxiosParamCreator = function (configuration?: Config }; }, /** - * Returns a paginated JSON list of stored run events. Ascending order uses `since_seq` as an inclusive cursor. Descending order uses `before_seq` as an exclusive cursor and starts at the newest event when `before_seq` is omitted. + * Returns a paginated JSON list of the run\'s events. The shape depends on the engine the run was created for (`RunSpec.engine`). For a legacy run (`engine.kind = legacy`): stored run events in the legacy envelope (`PaginatedEventList`). Ascending order uses `since_seq` as an inclusive cursor. Descending order uses `before_seq` as an exclusive cursor and starts at the newest event when `before_seq` is omitted. For a Petri run (`engine.kind = petri`): the run stream (`PaginatedRunStreamList`), one ordered delivery of Petri\'s own `RunEvent`s and Fabro\'s platform records in the `RunStreamItem` envelope, in `stream_seq` order. The cursor is `after`: the last `stream_seq` the client saw, exclusive; the first page is `after=0`. `since_seq`, `before_seq` and `order` are not accepted for a Petri run. A client that reconnects resumes from its last `stream_seq` and deduplicates by each item\'s `id`; every item is delivered once, in order, with no gap. * @summary List Run Events * @param {string} id Unique run identifier (ULID). * @param {number} [sinceSeq] First event sequence number to include. * @param {number} [limit] Maximum number of events to return. * @param {number} [beforeSeq] Exclusive upper event sequence cursor for descending order. Omit on the first descending request to start from the newest event. * @param {ListRunEventsOrderEnum} [order] Event sequence order. `since_seq` is valid only with `asc`; `before_seq` is valid only with `desc`. + * @param {number} [after] Run stream cursor for a Petri run: the last `stream_seq` the client saw, exclusive. `0` starts at the first item. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - listRunEvents: async (id: string, sinceSeq?: number, limit?: number, beforeSeq?: number, order?: ListRunEventsOrderEnum, options: RawAxiosRequestConfig = {}): Promise => { + listRunEvents: async (id: string, sinceSeq?: number, limit?: number, beforeSeq?: number, order?: ListRunEventsOrderEnum, after?: number, options: RawAxiosRequestConfig = {}): Promise => { // verify required parameter 'id' is not null or undefined assertParamExists('listRunEvents', 'id', id) const localVarPath = `/api/v1/runs/{id}/events` @@ -759,6 +767,10 @@ export const RunInternalsApiAxiosParamCreator = function (configuration?: Config localVarQueryParameter['order'] = order; } + if (after !== undefined) { + localVarQueryParameter['after'] = after; + } + localVarHeaderParameter['Accept'] = 'application/json'; setSearchParams(localVarUrlObj, localVarQueryParameter); @@ -1386,15 +1398,16 @@ export const RunInternalsApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** - * Opens an ordered server-sent event stream starting at `since_seq`, replaying persisted events and continuing with live updates while the run remains active. + * Opens an ordered server-sent event stream, replaying persisted items and continuing with live updates while the run remains active. Each `data:` frame is one JSON object in the run engine\'s envelope. For a legacy run the frames are `EventEnvelope`s and the stream starts at `since_seq` (inclusive; the next unseen event when omitted). It ends after `run.completed` or `run.failed`. For a Petri run the frames are `RunStreamItem`s and the stream starts after `after` (the last `stream_seq` the client saw; `0` replays the whole run; the next unseen item when omitted). It ends once the run is no longer active and every committed item has been sent. A reconnecting client passes its last `stream_seq` as `after` and deduplicates by `id`. * @summary Attach Run Events * @param {string} id Unique run identifier (ULID). * @param {number} [sinceSeq] First event sequence number to include. + * @param {number} [after] Run stream cursor for a Petri run: the last `stream_seq` the client saw, exclusive. `0` starts at the first item. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async attachRunEvents(id: string, sinceSeq?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.attachRunEvents(id, sinceSeq, options); + async attachRunEvents(id: string, sinceSeq?: number, after?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.attachRunEvents(id, sinceSeq, after, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['RunInternalsApi.attachRunEvents']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); @@ -1541,18 +1554,19 @@ export const RunInternalsApiFp = function(configuration?: Configuration) { return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, /** - * Returns a paginated JSON list of stored run events. Ascending order uses `since_seq` as an inclusive cursor. Descending order uses `before_seq` as an exclusive cursor and starts at the newest event when `before_seq` is omitted. + * Returns a paginated JSON list of the run\'s events. The shape depends on the engine the run was created for (`RunSpec.engine`). For a legacy run (`engine.kind = legacy`): stored run events in the legacy envelope (`PaginatedEventList`). Ascending order uses `since_seq` as an inclusive cursor. Descending order uses `before_seq` as an exclusive cursor and starts at the newest event when `before_seq` is omitted. For a Petri run (`engine.kind = petri`): the run stream (`PaginatedRunStreamList`), one ordered delivery of Petri\'s own `RunEvent`s and Fabro\'s platform records in the `RunStreamItem` envelope, in `stream_seq` order. The cursor is `after`: the last `stream_seq` the client saw, exclusive; the first page is `after=0`. `since_seq`, `before_seq` and `order` are not accepted for a Petri run. A client that reconnects resumes from its last `stream_seq` and deduplicates by each item\'s `id`; every item is delivered once, in order, with no gap. * @summary List Run Events * @param {string} id Unique run identifier (ULID). * @param {number} [sinceSeq] First event sequence number to include. * @param {number} [limit] Maximum number of events to return. * @param {number} [beforeSeq] Exclusive upper event sequence cursor for descending order. Omit on the first descending request to start from the newest event. * @param {ListRunEventsOrderEnum} [order] Event sequence order. `since_seq` is valid only with `asc`; `before_seq` is valid only with `desc`. + * @param {number} [after] Run stream cursor for a Petri run: the last `stream_seq` the client saw, exclusive. `0` starts at the first item. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async listRunEvents(id: string, sinceSeq?: number, limit?: number, beforeSeq?: number, order?: ListRunEventsOrderEnum, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.listRunEvents(id, sinceSeq, limit, beforeSeq, order, options); + async listRunEvents(id: string, sinceSeq?: number, limit?: number, beforeSeq?: number, order?: ListRunEventsOrderEnum, after?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.listRunEvents(id, sinceSeq, limit, beforeSeq, order, after, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['RunInternalsApi.listRunEvents']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); @@ -1774,15 +1788,16 @@ export const RunInternalsApiFactory = function (configuration?: Configuration, b return localVarFp.appendRunEvent(id, runEvent, options).then((request) => request(axios, basePath)); }, /** - * Opens an ordered server-sent event stream starting at `since_seq`, replaying persisted events and continuing with live updates while the run remains active. + * Opens an ordered server-sent event stream, replaying persisted items and continuing with live updates while the run remains active. Each `data:` frame is one JSON object in the run engine\'s envelope. For a legacy run the frames are `EventEnvelope`s and the stream starts at `since_seq` (inclusive; the next unseen event when omitted). It ends after `run.completed` or `run.failed`. For a Petri run the frames are `RunStreamItem`s and the stream starts after `after` (the last `stream_seq` the client saw; `0` replays the whole run; the next unseen item when omitted). It ends once the run is no longer active and every committed item has been sent. A reconnecting client passes its last `stream_seq` as `after` and deduplicates by `id`. * @summary Attach Run Events * @param {string} id Unique run identifier (ULID). * @param {number} [sinceSeq] First event sequence number to include. + * @param {number} [after] Run stream cursor for a Petri run: the last `stream_seq` the client saw, exclusive. `0` starts at the first item. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - attachRunEvents(id: string, sinceSeq?: number, options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.attachRunEvents(id, sinceSeq, options).then((request) => request(axios, basePath)); + attachRunEvents(id: string, sinceSeq?: number, after?: number, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.attachRunEvents(id, sinceSeq, after, options).then((request) => request(axios, basePath)); }, /** * Streams a ZIP archive with the latest captured version of each artifact path. Stage order, retry number, and then stage ID determine the latest version, matching the artifacts page. Captures from the graph\'s boundary nodes are excluded, identified by their `start` and `exit` handler type rather than by node name. The archive streams, so the response status is sent before the first artifact is read. A failure after that point aborts the transfer rather than returning `500`. The ZIP central directory is written last, so a truncated download does not open as a valid archive. @@ -1896,18 +1911,19 @@ export const RunInternalsApiFactory = function (configuration?: Configuration, b return localVarFp.listRunArtifacts(id, options).then((request) => request(axios, basePath)); }, /** - * Returns a paginated JSON list of stored run events. Ascending order uses `since_seq` as an inclusive cursor. Descending order uses `before_seq` as an exclusive cursor and starts at the newest event when `before_seq` is omitted. + * Returns a paginated JSON list of the run\'s events. The shape depends on the engine the run was created for (`RunSpec.engine`). For a legacy run (`engine.kind = legacy`): stored run events in the legacy envelope (`PaginatedEventList`). Ascending order uses `since_seq` as an inclusive cursor. Descending order uses `before_seq` as an exclusive cursor and starts at the newest event when `before_seq` is omitted. For a Petri run (`engine.kind = petri`): the run stream (`PaginatedRunStreamList`), one ordered delivery of Petri\'s own `RunEvent`s and Fabro\'s platform records in the `RunStreamItem` envelope, in `stream_seq` order. The cursor is `after`: the last `stream_seq` the client saw, exclusive; the first page is `after=0`. `since_seq`, `before_seq` and `order` are not accepted for a Petri run. A client that reconnects resumes from its last `stream_seq` and deduplicates by each item\'s `id`; every item is delivered once, in order, with no gap. * @summary List Run Events * @param {string} id Unique run identifier (ULID). * @param {number} [sinceSeq] First event sequence number to include. * @param {number} [limit] Maximum number of events to return. * @param {number} [beforeSeq] Exclusive upper event sequence cursor for descending order. Omit on the first descending request to start from the newest event. * @param {ListRunEventsOrderEnum} [order] Event sequence order. `since_seq` is valid only with `asc`; `before_seq` is valid only with `desc`. + * @param {number} [after] Run stream cursor for a Petri run: the last `stream_seq` the client saw, exclusive. `0` starts at the first item. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - listRunEvents(id: string, sinceSeq?: number, limit?: number, beforeSeq?: number, order?: ListRunEventsOrderEnum, options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.listRunEvents(id, sinceSeq, limit, beforeSeq, order, options).then((request) => request(axios, basePath)); + listRunEvents(id: string, sinceSeq?: number, limit?: number, beforeSeq?: number, order?: ListRunEventsOrderEnum, after?: number, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.listRunEvents(id, sinceSeq, limit, beforeSeq, order, after, options).then((request) => request(axios, basePath)); }, /** * Returns the ordered list of stages in a run\'s workflow graph with their current status and timing. Stages are bounded by the workflow graph size, typically fewer than 20. @@ -2091,15 +2107,16 @@ export class RunInternalsApi extends BaseAPI { } /** - * Opens an ordered server-sent event stream starting at `since_seq`, replaying persisted events and continuing with live updates while the run remains active. + * Opens an ordered server-sent event stream, replaying persisted items and continuing with live updates while the run remains active. Each `data:` frame is one JSON object in the run engine\'s envelope. For a legacy run the frames are `EventEnvelope`s and the stream starts at `since_seq` (inclusive; the next unseen event when omitted). It ends after `run.completed` or `run.failed`. For a Petri run the frames are `RunStreamItem`s and the stream starts after `after` (the last `stream_seq` the client saw; `0` replays the whole run; the next unseen item when omitted). It ends once the run is no longer active and every committed item has been sent. A reconnecting client passes its last `stream_seq` as `after` and deduplicates by `id`. * @summary Attach Run Events * @param {string} id Unique run identifier (ULID). * @param {number} [sinceSeq] First event sequence number to include. + * @param {number} [after] Run stream cursor for a Petri run: the last `stream_seq` the client saw, exclusive. `0` starts at the first item. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - public attachRunEvents(id: string, sinceSeq?: number, options?: RawAxiosRequestConfig) { - return RunInternalsApiFp(this.configuration).attachRunEvents(id, sinceSeq, options).then((request) => request(this.axios, this.basePath)); + public attachRunEvents(id: string, sinceSeq?: number, after?: number, options?: RawAxiosRequestConfig) { + return RunInternalsApiFp(this.configuration).attachRunEvents(id, sinceSeq, after, options).then((request) => request(this.axios, this.basePath)); } /** @@ -2224,18 +2241,19 @@ export class RunInternalsApi extends BaseAPI { } /** - * Returns a paginated JSON list of stored run events. Ascending order uses `since_seq` as an inclusive cursor. Descending order uses `before_seq` as an exclusive cursor and starts at the newest event when `before_seq` is omitted. + * Returns a paginated JSON list of the run\'s events. The shape depends on the engine the run was created for (`RunSpec.engine`). For a legacy run (`engine.kind = legacy`): stored run events in the legacy envelope (`PaginatedEventList`). Ascending order uses `since_seq` as an inclusive cursor. Descending order uses `before_seq` as an exclusive cursor and starts at the newest event when `before_seq` is omitted. For a Petri run (`engine.kind = petri`): the run stream (`PaginatedRunStreamList`), one ordered delivery of Petri\'s own `RunEvent`s and Fabro\'s platform records in the `RunStreamItem` envelope, in `stream_seq` order. The cursor is `after`: the last `stream_seq` the client saw, exclusive; the first page is `after=0`. `since_seq`, `before_seq` and `order` are not accepted for a Petri run. A client that reconnects resumes from its last `stream_seq` and deduplicates by each item\'s `id`; every item is delivered once, in order, with no gap. * @summary List Run Events * @param {string} id Unique run identifier (ULID). * @param {number} [sinceSeq] First event sequence number to include. * @param {number} [limit] Maximum number of events to return. * @param {number} [beforeSeq] Exclusive upper event sequence cursor for descending order. Omit on the first descending request to start from the newest event. * @param {ListRunEventsOrderEnum} [order] Event sequence order. `since_seq` is valid only with `asc`; `before_seq` is valid only with `desc`. + * @param {number} [after] Run stream cursor for a Petri run: the last `stream_seq` the client saw, exclusive. `0` starts at the first item. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - public listRunEvents(id: string, sinceSeq?: number, limit?: number, beforeSeq?: number, order?: ListRunEventsOrderEnum, options?: RawAxiosRequestConfig) { - return RunInternalsApiFp(this.configuration).listRunEvents(id, sinceSeq, limit, beforeSeq, order, options).then((request) => request(this.axios, this.basePath)); + public listRunEvents(id: string, sinceSeq?: number, limit?: number, beforeSeq?: number, order?: ListRunEventsOrderEnum, after?: number, options?: RawAxiosRequestConfig) { + return RunInternalsApiFp(this.configuration).listRunEvents(id, sinceSeq, limit, beforeSeq, order, after, options).then((request) => request(this.axios, this.basePath)); } /** diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index bb9d7664e..96c56b604 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -186,6 +186,7 @@ export * from './interview-option'; export * from './interview-provider-settings'; export * from './interview-question-record'; export * from './link-run-pull-request-request'; +export * from './list-run-events200-response'; export * from './llm-output-kind'; export * from './llm-retry-classification'; export * from './llm-retry-classification-after'; @@ -241,6 +242,7 @@ export * from './paginated-run-commit-list'; export * from './paginated-run-file-list'; export * from './paginated-run-list'; export * from './paginated-run-stage-list'; +export * from './paginated-run-stream-list'; export * from './paginated-saved-query-list'; export * from './paginated-session-list'; export * from './paginated-workflow-list-response'; @@ -266,7 +268,9 @@ export * from './pending-interview-record'; export * from './pending-reason'; export * from './permission-level'; export * from './petri-access'; +export * from './petri-admission'; export * from './petri-append-request'; +export * from './petri-graph-ref'; export * from './petri-open-request'; export * from './petri-open-response'; export * from './petri-platform-record'; @@ -357,6 +361,9 @@ export * from './run-commit-person'; export * from './run-commits-meta'; export * from './run-control-action'; export * from './run-diff'; +export * from './run-engine'; +export * from './run-engine-one-of'; +export * from './run-engine-one-of1'; export * from './run-environment-settings'; export * from './run-error'; export * from './run-event'; @@ -416,6 +423,8 @@ export * from './run-status-running'; export * from './run-status-starting'; export * from './run-status-submitted'; export * from './run-status-succeeded'; +export * from './run-stream-item'; +export * from './run-stream-item-kind'; export * from './run-superseded-by-props'; export * from './run-target'; export * from './run-timestamps'; diff --git a/lib/packages/fabro-api-client/src/models/list-run-events200-response.ts b/lib/packages/fabro-api-client/src/models/list-run-events200-response.ts new file mode 100644 index 000000000..ed35f86ed --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/list-run-events200-response.ts @@ -0,0 +1,32 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.2.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { PaginatedEventList } from './paginated-event-list'; +// May contain unused imports in some cases +// @ts-ignore +import type { PaginatedRunStreamList } from './paginated-run-stream-list'; +// May contain unused imports in some cases +// @ts-ignore +import type { PaginationMeta } from './pagination-meta'; +// May contain unused imports in some cases +// @ts-ignore +import type { RunStreamItem } from './run-stream-item'; + +/** + * @type ListRunEvents200Response + */ +export type ListRunEvents200Response = PaginatedEventList | PaginatedRunStreamList; diff --git a/lib/packages/fabro-api-client/src/models/paginated-run-stream-list.ts b/lib/packages/fabro-api-client/src/models/paginated-run-stream-list.ts new file mode 100644 index 000000000..878abf91c --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/paginated-run-stream-list.ts @@ -0,0 +1,30 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.2.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { PaginationMeta } from './pagination-meta'; +// May contain unused imports in some cases +// @ts-ignore +import type { RunStreamItem } from './run-stream-item'; + +/** + * One page of a Petri run\'s stream, in `stream_seq` order. `event_contract_version` is Petri\'s `EVENT_CONTRACT_VERSION` the server was built against: the version of the event contract every `petri` item follows. + */ +export interface PaginatedRunStreamList { + 'data': Array; + 'meta': PaginationMeta; + 'event_contract_version': number; +} diff --git a/lib/packages/fabro-api-client/src/models/petri-admission.ts b/lib/packages/fabro-api-client/src/models/petri-admission.ts new file mode 100644 index 000000000..744c46a97 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/petri-admission.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.2.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { PetriGraphRef } from './petri-graph-ref'; + +/** + * What Petri admitted for a run at create time: the lowered root graph and the pre-lowered child graphs, every one persisted in the blob store before the run exists. + */ +export interface PetriAdmission { + 'graph': PetriGraphRef; + 'children'?: Array; +} diff --git a/lib/packages/fabro-api-client/src/models/petri-graph-ref.ts b/lib/packages/fabro-api-client/src/models/petri-graph-ref.ts new file mode 100644 index 000000000..3eb77a1ea --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/petri-graph-ref.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.2.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * An admitted graph in the blob store, verified by digest on load. + */ +export interface PetriGraphRef { + /** + * Content-addressed SHA-256 hash of a stored blob. Hex input is case-insensitive; Fabro emits the canonical lowercase form. + */ + 'blob': string; + 'digest': string; +} diff --git a/lib/packages/fabro-api-client/src/models/run-engine-one-of.ts b/lib/packages/fabro-api-client/src/models/run-engine-one-of.ts new file mode 100644 index 000000000..86bf37439 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/run-engine-one-of.ts @@ -0,0 +1,25 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.2.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export interface RunEngineOneOf { + 'kind': RunEngineOneOfKindEnum; +} + +export const RunEngineOneOfKindEnum = { + LEGACY: 'legacy' +} as const; + +export type RunEngineOneOfKindEnum = typeof RunEngineOneOfKindEnum[keyof typeof RunEngineOneOfKindEnum]; diff --git a/lib/packages/fabro-api-client/src/models/run-engine-one-of1.ts b/lib/packages/fabro-api-client/src/models/run-engine-one-of1.ts new file mode 100644 index 000000000..e4ab32ac8 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/run-engine-one-of1.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.2.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { PetriAdmission } from './petri-admission'; +// May contain unused imports in some cases +// @ts-ignore +import type { PetriGraphRef } from './petri-graph-ref'; + +/** + * @type RunEngineOneOf1 + */ +export type RunEngineOneOf1 = PetriAdmission; diff --git a/lib/packages/fabro-api-client/src/models/run-engine.ts b/lib/packages/fabro-api-client/src/models/run-engine.ts new file mode 100644 index 000000000..c8f4929c8 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/run-engine.ts @@ -0,0 +1,30 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.2.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { PetriGraphRef } from './petri-graph-ref'; +// May contain unused imports in some cases +// @ts-ignore +import type { RunEngineOneOf } from './run-engine-one-of'; +// May contain unused imports in some cases +// @ts-ignore +import type { RunEngineOneOf1 } from './run-engine-one-of1'; + +/** + * @type RunEngine + * The engine a run was created for. `legacy` is the in-process executor; `petri` names the Petri workflow engine and carries what Petri admitted at create time. + */ +export type RunEngine = RunEngineOneOf | RunEngineOneOf1; diff --git a/lib/packages/fabro-api-client/src/models/run-spec.ts b/lib/packages/fabro-api-client/src/models/run-spec.ts index 6c05283aa..b8e169a99 100644 --- a/lib/packages/fabro-api-client/src/models/run-spec.ts +++ b/lib/packages/fabro-api-client/src/models/run-spec.ts @@ -24,6 +24,9 @@ import type { ForkSourceRef } from './fork-source-ref'; import type { GitContext } from './git-context'; // May contain unused imports in some cases // @ts-ignore +import type { RunEngine } from './run-engine'; +// May contain unused imports in some cases +// @ts-ignore import type { RunProvenance } from './run-provenance'; // May contain unused imports in some cases // @ts-ignore @@ -54,4 +57,8 @@ export interface RunSpec { 'spec_blob'?: string | null; 'git'?: GitContext | null; 'fork_source_ref'?: ForkSourceRef | null; + /** + * The engine the run was created for, with what it admitted. Absent in a spec written before the field existed, which means the legacy executor. + */ + 'engine'?: RunEngine; } diff --git a/lib/packages/fabro-api-client/src/models/run-stream-item-kind.ts b/lib/packages/fabro-api-client/src/models/run-stream-item-kind.ts new file mode 100644 index 000000000..f81e14391 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/run-stream-item-kind.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.2.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * Which item shape a run stream item carries. + */ + +export const RunStreamItemKind = { + PETRI: 'petri', + PLATFORM: 'platform' +} as const; + +export type RunStreamItemKind = typeof RunStreamItemKind[keyof typeof RunStreamItemKind]; diff --git a/lib/packages/fabro-api-client/src/models/run-stream-item.ts b/lib/packages/fabro-api-client/src/models/run-stream-item.ts new file mode 100644 index 000000000..b73e6120b --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/run-stream-item.ts @@ -0,0 +1,42 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.2.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { RunStreamItemKind } from './run-stream-item-kind'; + +/** + * One item of a Petri run\'s stream: a Petri `RunEvent` or a Fabro platform record in Fabro\'s envelope. `stream_seq` is the durable per-run delivery sequence the projector assigned when the item\'s record was committed: dense, strictly increasing within the run, and the cursor for `after`. `id` is the item\'s own identity, kept beside the cursor so a client deduplicates by it: for a Petri event the `EventId` as `//` (`coordinator/3/0`, `execution 1/23/0`); for a platform record its `seq`. Petri\'s `EventId` is per log and has no platform variant, so it is never the cursor. A `petri` item is a Petri `RunEvent` passed through unchanged: `{id: {log, execution?, seq, index}, origin, recorded_at, observed_at?, context: {invocation, execution, parent?}, subject?, record?, derived?}`. Its vocabulary is Petri\'s public event contract (`crates/core/execution/EVENTS.md` in the Petri repository), not Fabro\'s: the recorded event\'s name is `record.body.event` (`.`, e.g. `visit.started`, `step.finished`, `run.finished`), a derived view event\'s is `derived.event`, and the stage a subject names is `(context.execution, subject.firing)` with `subject.node.name` and `subject.visit` as its display label. The server reports the contract version it serves in `PaginatedRunStreamList.event_contract_version`. A `platform` item is a stored platform record: `{seq, recorded_at, record: {kind, ...}, position?: {execution, firing}}`. `record.kind` is one of `run.created`, `run.lifecycle`, `run.title`, `run.parent`, `run.archived`, `run.unarchived`, `run.superseded`, `run.notice`, `interview.answered`, `run.branch`, `git.identity`, `checkpoint`, `pull_request.created`, `notification.sent`, `run.paired`. + */ +export interface RunStreamItem { + 'run_id': string; + /** + * The delivery sequence; the cursor. + */ + 'stream_seq': number; + 'kind': RunStreamItemKind; + /** + * The item\'s own identity, for deduplication. + */ + 'id': string; + /** + * Milliseconds since the Unix epoch when the item\'s record was appended. + */ + 'recorded_at': number; + /** + * The Petri `RunEvent` or the stored platform record, unchanged. + */ + 'item': { [key: string]: any; }; +}