@osolmaz/pi-workflows 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -26
- package/dist/builtins/monitor.workflow.d.ts +71 -0
- package/dist/builtins/monitor.workflow.js +234 -0
- package/dist/builtins/monitor.workflow.js.map +1 -0
- package/dist/controllers/conditions.d.ts +6 -0
- package/dist/controllers/conditions.js +68 -0
- package/dist/controllers/conditions.js.map +1 -0
- package/dist/controllers/definition.d.ts +6 -0
- package/dist/controllers/definition.js +45 -0
- package/dist/controllers/definition.js.map +1 -0
- package/dist/controllers/effects.d.ts +14 -0
- package/dist/controllers/effects.js +104 -0
- package/dist/controllers/effects.js.map +1 -0
- package/dist/controllers/errors.d.ts +12 -0
- package/dist/controllers/errors.js +25 -0
- package/dist/controllers/errors.js.map +1 -0
- package/dist/controllers/index.d.ts +13 -0
- package/dist/controllers/index.js +13 -0
- package/dist/controllers/index.js.map +1 -0
- package/dist/controllers/json.d.ts +5 -0
- package/dist/controllers/json.js +57 -0
- package/dist/controllers/json.js.map +1 -0
- package/dist/controllers/loader.d.ts +23 -0
- package/dist/controllers/loader.js +74 -0
- package/dist/controllers/loader.js.map +1 -0
- package/dist/controllers/manager.d.ts +58 -0
- package/dist/controllers/manager.js +399 -0
- package/dist/controllers/manager.js.map +1 -0
- package/dist/controllers/results.d.ts +5 -0
- package/dist/controllers/results.js +32 -0
- package/dist/controllers/results.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +212 -0
- package/dist/controllers/sqlite.js +1009 -0
- package/dist/controllers/sqlite.js.map +1 -0
- package/dist/controllers/store.d.ts +112 -0
- package/dist/controllers/store.js +32 -0
- package/dist/controllers/store.js.map +1 -0
- package/dist/controllers/types.d.ts +159 -0
- package/dist/controllers/types.js +2 -0
- package/dist/controllers/types.js.map +1 -0
- package/dist/controllers/workflow-engine-scheduler.d.ts +25 -0
- package/dist/controllers/workflow-engine-scheduler.js +93 -0
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -0
- package/dist/controllers/workflows.d.ts +27 -0
- package/dist/controllers/workflows.js +109 -0
- package/dist/controllers/workflows.js.map +1 -0
- package/dist/extension/controller-host.d.ts +47 -0
- package/dist/extension/controller-host.js +110 -0
- package/dist/extension/controller-host.js.map +1 -0
- package/dist/extension/executor.d.ts +14 -1
- package/dist/extension/executor.js +12 -2
- package/dist/extension/executor.js.map +1 -1
- package/dist/extension/index.d.ts +7 -0
- package/dist/extension/index.js +1118 -89
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +91 -0
- package/dist/extension/recorder.js +537 -0
- package/dist/extension/recorder.js.map +1 -0
- package/dist/extension/session-events.d.ts +134 -0
- package/dist/extension/session-events.js +60 -0
- package/dist/extension/session-events.js.map +1 -0
- package/dist/extension/widget.js +25 -24
- package/dist/extension/widget.js.map +1 -1
- package/dist/extension/workflow-tool.d.ts +28 -0
- package/dist/extension/workflow-tool.js +33 -0
- package/dist/extension/workflow-tool.js.map +1 -0
- package/dist/host/processes.d.ts +24 -0
- package/dist/host/processes.js +114 -0
- package/dist/host/processes.js.map +1 -0
- package/dist/host/rpc-bridge.d.ts +9 -0
- package/dist/host/rpc-bridge.js +39 -0
- package/dist/host/rpc-bridge.js.map +1 -0
- package/dist/host/rpc-executor.d.ts +38 -0
- package/dist/host/rpc-executor.js +254 -0
- package/dist/host/rpc-executor.js.map +1 -0
- package/dist/host/runner.d.ts +49 -0
- package/dist/host/runner.js +350 -0
- package/dist/host/runner.js.map +1 -0
- package/dist/render/canvas.d.ts +1 -1
- package/dist/render/canvas.js +5 -0
- package/dist/render/canvas.js.map +1 -1
- package/dist/render/graph-render.d.ts +5 -0
- package/dist/render/graph-render.js +211 -48
- package/dist/render/graph-render.js.map +1 -1
- package/dist/viewer/cli.d.ts +7 -3
- package/dist/viewer/cli.js +150 -19
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.js +19 -3
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/session-reducer.d.ts +45 -0
- package/dist/viewer/session-reducer.js +266 -0
- package/dist/viewer/session-reducer.js.map +1 -0
- package/dist/workflows/artifacts.d.ts +40 -0
- package/dist/workflows/artifacts.js +155 -0
- package/dist/workflows/artifacts.js.map +1 -0
- package/dist/workflows/engine.d.ts +38 -0
- package/dist/workflows/engine.js +281 -20
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +23 -0
- package/dist/workflows/errors.js +38 -0
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/graph.js +0 -5
- package/dist/workflows/graph.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -2
- package/dist/workflows/index.js +2 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/loader.d.ts +5 -3
- package/dist/workflows/loader.js +10 -1
- package/dist/workflows/loader.js.map +1 -1
- package/dist/workflows/schema.js +1 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +98 -10
- package/dist/workflows/store.js +921 -46
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +136 -3
- package/docs/CONTROLLERS.md +215 -0
- package/docs/development.md +51 -24
- package/docs/live-replay-protocol.md +155 -0
- package/docs/plans/2026-08-04-controller-runtime-plan.md +169 -0
- package/docs/plans/2026-08-05-always-on-workflows-plan.md +125 -0
- package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +184 -0
- package/docs/plans/piw-viewer-experience-implementation-plan.md +674 -0
- package/docs/plans/replayable-run-bundles-implementation-plan.md +65 -0
- package/docs/plans/session-event-replay-implementation-plan.md +494 -0
- package/docs/plans/tui-viewer-implementation-plan.md +64 -0
- package/docs/run-bundles.md +359 -55
- package/docs/session-event-journal.md +470 -0
- package/docs/tui-viewer.md +218 -0
- package/docs/workflows.md +131 -11
- package/examples/controllers/pull-request.controller.ts +215 -0
- package/package.json +11 -2
- package/src/builtins/monitor.workflow.ts +278 -0
- package/src/controllers/conditions.ts +110 -0
- package/src/controllers/definition.ts +65 -0
- package/src/controllers/effects.ts +123 -0
- package/src/controllers/errors.ts +27 -0
- package/src/controllers/index.ts +90 -0
- package/src/controllers/json.ts +62 -0
- package/src/controllers/loader.ts +104 -0
- package/src/controllers/manager.ts +533 -0
- package/src/controllers/results.ts +46 -0
- package/src/controllers/sqlite.ts +1427 -0
- package/src/controllers/store.ts +160 -0
- package/src/controllers/types.ts +183 -0
- package/src/controllers/workflow-engine-scheduler.ts +145 -0
- package/src/controllers/workflows.ts +152 -0
- package/src/extension/controller-host.ts +163 -0
- package/src/extension/executor.ts +29 -2
- package/src/extension/index.ts +1315 -109
- package/src/extension/recorder.ts +662 -0
- package/src/extension/session-events.ts +119 -0
- package/src/extension/widget.ts +26 -24
- package/src/extension/workflow-tool.ts +59 -0
- package/src/host/processes.ts +119 -0
- package/src/host/rpc-bridge.ts +44 -0
- package/src/host/rpc-executor.ts +299 -0
- package/src/host/runner.ts +406 -0
- package/src/render/canvas.ts +19 -1
- package/src/render/graph-render.ts +277 -44
- package/src/viewer/cli.ts +167 -21
- package/src/viewer/render.ts +21 -3
- package/src/viewer/session-reducer.ts +347 -0
- package/src/workflows/artifacts.ts +188 -0
- package/src/workflows/engine.ts +365 -19
- package/src/workflows/errors.ts +45 -0
- package/src/workflows/graph.ts +0 -5
- package/src/workflows/index.ts +15 -0
- package/src/workflows/loader.ts +13 -3
- package/src/workflows/schema.ts +1 -1
- package/src/workflows/store.ts +1157 -48
- package/src/workflows/types.ts +151 -3
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
export type PublicAssistantMessageEvent =
|
|
2
|
+
| { type: "start"; partial: unknown }
|
|
3
|
+
| { type: "text_start"; contentIndex: number; partial: unknown }
|
|
4
|
+
| { type: "text_delta"; contentIndex: number; delta: string; partial: unknown }
|
|
5
|
+
| { type: "text_end"; contentIndex: number; content: string; partial: unknown }
|
|
6
|
+
| { type: "thinking_start"; contentIndex: number; partial: unknown }
|
|
7
|
+
| { type: "thinking_delta"; contentIndex: number; delta: string; partial: unknown }
|
|
8
|
+
| { type: "thinking_end"; contentIndex: number; content: string; partial: unknown }
|
|
9
|
+
| { type: "toolcall_start"; contentIndex: number; partial: unknown }
|
|
10
|
+
| { type: "toolcall_delta"; contentIndex: number; delta: string; partial: unknown }
|
|
11
|
+
| { type: "toolcall_end"; contentIndex: number; toolCall: unknown; partial: unknown }
|
|
12
|
+
| { type: "done"; reason: "stop" | "length" | "toolUse"; message: unknown }
|
|
13
|
+
| { type: "error"; reason: "aborted" | "error"; error: unknown };
|
|
14
|
+
|
|
15
|
+
export type TurnStartEventLike = { turnIndex: number };
|
|
16
|
+
export type TurnEndEventLike = { turnIndex: number; message: unknown };
|
|
17
|
+
export type MessageStartEventLike = { message: unknown };
|
|
18
|
+
export type MessageUpdateEventLike = {
|
|
19
|
+
message: unknown;
|
|
20
|
+
assistantMessageEvent: PublicAssistantMessageEvent;
|
|
21
|
+
};
|
|
22
|
+
export type MessageEndEventLike = { message: unknown };
|
|
23
|
+
export type ToolExecutionStartEventLike = {
|
|
24
|
+
toolCallId: string;
|
|
25
|
+
toolName: string;
|
|
26
|
+
args: unknown;
|
|
27
|
+
};
|
|
28
|
+
export type ToolExecutionUpdateEventLike = { toolCallId: string };
|
|
29
|
+
export type ToolExecutionEndEventLike = {
|
|
30
|
+
toolCallId: string;
|
|
31
|
+
toolName: string;
|
|
32
|
+
result: unknown;
|
|
33
|
+
isError: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type NormalizedAssistantEvent =
|
|
37
|
+
| { type: "start" }
|
|
38
|
+
| { type: "text_start"; contentIndex: number }
|
|
39
|
+
| { type: "text_delta"; contentIndex: number; delta: string }
|
|
40
|
+
| { type: "text_end"; contentIndex: number; content: string }
|
|
41
|
+
| { type: "thinking_start"; contentIndex: number }
|
|
42
|
+
| { type: "thinking_delta"; contentIndex: number; delta: string }
|
|
43
|
+
| { type: "thinking_end"; contentIndex: number; content: string }
|
|
44
|
+
| { type: "toolcall_start"; contentIndex: number }
|
|
45
|
+
| { type: "toolcall_delta"; contentIndex: number; delta: string }
|
|
46
|
+
| { type: "toolcall_end"; contentIndex: number; toolCall: unknown }
|
|
47
|
+
| { type: "done"; reason: "stop" | "length" | "toolUse" }
|
|
48
|
+
| { type: "error"; reason: "aborted" | "error" };
|
|
49
|
+
|
|
50
|
+
/** Remove cumulative snapshots while preserving every semantic stream event. */
|
|
51
|
+
export function normalizeAssistantEvent(
|
|
52
|
+
event: PublicAssistantMessageEvent,
|
|
53
|
+
): NormalizedAssistantEvent {
|
|
54
|
+
switch (event.type) {
|
|
55
|
+
case "start":
|
|
56
|
+
return { type: event.type };
|
|
57
|
+
case "text_start":
|
|
58
|
+
case "thinking_start":
|
|
59
|
+
case "toolcall_start":
|
|
60
|
+
return { type: event.type, contentIndex: event.contentIndex };
|
|
61
|
+
case "text_delta":
|
|
62
|
+
case "thinking_delta":
|
|
63
|
+
case "toolcall_delta":
|
|
64
|
+
return { type: event.type, contentIndex: event.contentIndex, delta: event.delta };
|
|
65
|
+
case "text_end":
|
|
66
|
+
case "thinking_end":
|
|
67
|
+
return { type: event.type, contentIndex: event.contentIndex, content: event.content };
|
|
68
|
+
case "toolcall_end":
|
|
69
|
+
return {
|
|
70
|
+
type: event.type,
|
|
71
|
+
contentIndex: event.contentIndex,
|
|
72
|
+
toolCall: event.toolCall,
|
|
73
|
+
};
|
|
74
|
+
case "done":
|
|
75
|
+
return { type: event.type, reason: event.reason };
|
|
76
|
+
case "error":
|
|
77
|
+
return { type: event.type, reason: event.reason };
|
|
78
|
+
default: {
|
|
79
|
+
const exhaustive: never = event;
|
|
80
|
+
return exhaustive;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function messageRole(message: unknown): string {
|
|
86
|
+
if (typeof message !== "object" || message === null || !("role" in message)) {
|
|
87
|
+
return "unknown";
|
|
88
|
+
}
|
|
89
|
+
const role = (message as { role?: unknown }).role;
|
|
90
|
+
return typeof role === "string" && role.length > 0 ? role : "unknown";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function toolCallIdFromAssistantEvent(event: NormalizedAssistantEvent): string | null {
|
|
94
|
+
if (event.type !== "toolcall_end" || typeof event.toolCall !== "object" || !event.toolCall) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const id = (event.toolCall as { id?: unknown }).id;
|
|
98
|
+
return typeof id === "string" && id.length > 0 ? id : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function toolStartedPayload(event: ToolExecutionStartEventLike): Record<string, unknown> {
|
|
102
|
+
return { toolName: event.toolName, args: event.args };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function toolFinishedPayload(event: ToolExecutionEndEventLike): Record<string, unknown> {
|
|
106
|
+
return { toolName: event.toolName, isError: event.isError, result: event.result };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function turnFinishedPayload(
|
|
110
|
+
event: TurnEndEventLike,
|
|
111
|
+
messageId: string | undefined,
|
|
112
|
+
toolCallIds: string[],
|
|
113
|
+
): Record<string, unknown> {
|
|
114
|
+
return {
|
|
115
|
+
turnIndex: event.turnIndex,
|
|
116
|
+
...(messageId === undefined ? {} : { messageId }),
|
|
117
|
+
toolCallIds,
|
|
118
|
+
};
|
|
119
|
+
}
|
package/src/extension/widget.ts
CHANGED
|
@@ -113,16 +113,24 @@ export function buildWidgetLines(
|
|
|
113
113
|
/** The graph row the window should center on: the active or waiting node. */
|
|
114
114
|
function focusLine(graph: string[], state: WorkflowRunState): number {
|
|
115
115
|
const active = graph.findIndex((line) => stripAnsi(line).includes("◐"));
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
const focus =
|
|
117
|
+
active !== -1
|
|
118
|
+
? active
|
|
119
|
+
: state.waitingOn
|
|
120
|
+
? graph.findIndex((line) => stripAnsi(line).includes(state.waitingOn as string))
|
|
121
|
+
: -1;
|
|
122
|
+
if (focus === -1) {
|
|
123
|
+
return graph.length - 1;
|
|
118
124
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return waiting;
|
|
123
|
-
}
|
|
125
|
+
let top = focus;
|
|
126
|
+
while (top > 0 && !/[┌┏]/u.test(stripAnsi(graph[top] ?? ""))) {
|
|
127
|
+
top -= 1;
|
|
124
128
|
}
|
|
125
|
-
|
|
129
|
+
let bottom = focus;
|
|
130
|
+
while (bottom + 1 < graph.length && !/[└┗]/u.test(stripAnsi(graph[bottom] ?? ""))) {
|
|
131
|
+
bottom += 1;
|
|
132
|
+
}
|
|
133
|
+
return Math.floor((top + bottom) / 2);
|
|
126
134
|
}
|
|
127
135
|
|
|
128
136
|
/**
|
|
@@ -139,24 +147,18 @@ function windowLines(
|
|
|
139
147
|
if (lines.length <= budget) {
|
|
140
148
|
return { lines, scroll: 0, maxScroll: 0 };
|
|
141
149
|
}
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
let inner = budget;
|
|
146
|
-
for (let pass = 0; pass < 2; pass += 1) {
|
|
147
|
-
const start = clampStart(anchor, inner, lines.length, anchorIsStart);
|
|
148
|
-
inner = budget - (start > 0 ? 1 : 0) - (start + inner < lines.length ? 1 : 0);
|
|
149
|
-
}
|
|
150
|
+
// Reserve one combined overflow row, leaving seven rows for a complete
|
|
151
|
+
// full card even when the widget also has an error footer.
|
|
152
|
+
const inner = Math.max(1, budget - 1);
|
|
150
153
|
const start = clampStart(anchor, inner, lines.length, anchorIsStart);
|
|
151
154
|
const end = start + inner;
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
155
|
+
const above = start;
|
|
156
|
+
const below = Math.max(0, lines.length - end);
|
|
157
|
+
const out = lines.slice(start, end);
|
|
158
|
+
const directions = [above > 0 ? `↑ ${above}` : "", below > 0 ? `↓ ${below}` : ""]
|
|
159
|
+
.filter(Boolean)
|
|
160
|
+
.join(" · ");
|
|
161
|
+
out.push(ansi.dim(`${directions} more · shift+↑/↓ scroll`));
|
|
160
162
|
return { lines: out, scroll: start, maxScroll: Math.max(0, lines.length - inner) };
|
|
161
163
|
}
|
|
162
164
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import { Type, type TSchema } from "typebox";
|
|
3
|
+
|
|
4
|
+
const noExtraProperties = { additionalProperties: false } as const;
|
|
5
|
+
|
|
6
|
+
export const WorkflowToolParameters: TSchema = Type.Union([
|
|
7
|
+
Type.Object(
|
|
8
|
+
{
|
|
9
|
+
action: StringEnum(["list"] as const),
|
|
10
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, description: "Workflow list offset" })),
|
|
11
|
+
},
|
|
12
|
+
noExtraProperties,
|
|
13
|
+
),
|
|
14
|
+
Type.Object(
|
|
15
|
+
{
|
|
16
|
+
action: StringEnum(["start"] as const),
|
|
17
|
+
workflow: Type.String({ description: "Discovered workflow name or workflow file path" }),
|
|
18
|
+
input: Type.Optional(Type.Unknown({ description: "Structured workflow input" })),
|
|
19
|
+
},
|
|
20
|
+
noExtraProperties,
|
|
21
|
+
),
|
|
22
|
+
Type.Object(
|
|
23
|
+
{
|
|
24
|
+
action: StringEnum(["status"] as const),
|
|
25
|
+
runId: Type.Optional(Type.String({ description: "Run id; omit for the active run" })),
|
|
26
|
+
},
|
|
27
|
+
noExtraProperties,
|
|
28
|
+
),
|
|
29
|
+
Type.Object({ action: StringEnum(["pause"] as const) }, noExtraProperties),
|
|
30
|
+
Type.Object({ action: StringEnum(["resume"] as const) }, noExtraProperties),
|
|
31
|
+
Type.Object({ action: StringEnum(["cancel"] as const) }, noExtraProperties),
|
|
32
|
+
Type.Object(
|
|
33
|
+
{
|
|
34
|
+
action: StringEnum(["answer"] as const),
|
|
35
|
+
input: Type.Unknown({ description: "Checkpoint answer" }),
|
|
36
|
+
runId: Type.Optional(Type.String({ description: "Waiting run id" })),
|
|
37
|
+
},
|
|
38
|
+
noExtraProperties,
|
|
39
|
+
),
|
|
40
|
+
Type.Object(
|
|
41
|
+
{
|
|
42
|
+
action: StringEnum(["submit"] as const),
|
|
43
|
+
step: Type.String({ description: "Step id from the workflow step contract" }),
|
|
44
|
+
attempt: Type.String({ description: "Attempt id from the workflow step contract" }),
|
|
45
|
+
output: Type.Unknown({ description: "Step output matching the expected shape" }),
|
|
46
|
+
},
|
|
47
|
+
noExtraProperties,
|
|
48
|
+
),
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
export type WorkflowToolInput =
|
|
52
|
+
| { action: "list"; offset?: number }
|
|
53
|
+
| { action: "start"; workflow: string; input?: unknown }
|
|
54
|
+
| { action: "status"; runId?: string }
|
|
55
|
+
| { action: "pause" }
|
|
56
|
+
| { action: "resume" }
|
|
57
|
+
| { action: "cancel" }
|
|
58
|
+
| { action: "answer"; input: unknown; runId?: string }
|
|
59
|
+
| { action: "submit"; step: string; attempt: string; output: unknown };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tracks headless child processes (spawned `pi --mode rpc` sessions) so a
|
|
6
|
+
* killed host never leaves orphans working. Children spawn in their own
|
|
7
|
+
* process group; the registry file lets a later host reap leftovers by
|
|
8
|
+
* killing the whole group. The file lives next to the project store and is
|
|
9
|
+
* only ever touched by one host at a time (the advisory lock ensures it).
|
|
10
|
+
*/
|
|
11
|
+
export class HostProcessRegistry {
|
|
12
|
+
private readonly filePath: string;
|
|
13
|
+
private readonly pids = new Set<number>();
|
|
14
|
+
|
|
15
|
+
constructor(storeDir: string) {
|
|
16
|
+
this.filePath = path.join(storeDir, "host.children.json");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
register(pid: number): void {
|
|
20
|
+
this.pids.add(pid);
|
|
21
|
+
this.persist();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
unregister(pid: number): void {
|
|
25
|
+
this.pids.delete(pid);
|
|
26
|
+
this.persist();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get size(): number {
|
|
30
|
+
return this.pids.size;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Kill every registered child's process group, escalating to SIGKILL. */
|
|
34
|
+
killAll(): void {
|
|
35
|
+
for (const pid of this.pids) {
|
|
36
|
+
killProcessGroup(pid, "SIGTERM");
|
|
37
|
+
}
|
|
38
|
+
for (const pid of this.pids) {
|
|
39
|
+
killProcessGroup(pid, "SIGKILL");
|
|
40
|
+
}
|
|
41
|
+
this.pids.clear();
|
|
42
|
+
this.persist();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Reap children recorded by a previous host that died without cleanup.
|
|
47
|
+
* Called once at host start, before the lock is taken.
|
|
48
|
+
*/
|
|
49
|
+
reapOrphans(): number[] {
|
|
50
|
+
const recorded = this.readFile();
|
|
51
|
+
const reaped: number[] = [];
|
|
52
|
+
const stillAlive: number[] = [];
|
|
53
|
+
for (const pid of recorded) {
|
|
54
|
+
if (isAlive(pid)) {
|
|
55
|
+
killProcessGroup(pid, "SIGKILL");
|
|
56
|
+
reaped.push(pid);
|
|
57
|
+
if (isAlive(pid)) {
|
|
58
|
+
// A group kill that left the leader alive stays registered so a
|
|
59
|
+
// later start tries again instead of forgetting it.
|
|
60
|
+
stillAlive.push(pid);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
this.pids.clear();
|
|
65
|
+
for (const pid of stillAlive) {
|
|
66
|
+
this.pids.add(pid);
|
|
67
|
+
}
|
|
68
|
+
this.persist();
|
|
69
|
+
return reaped;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private readFile(): number[] {
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(fs.readFileSync(this.filePath, "utf8")) as unknown;
|
|
75
|
+
if (!Array.isArray(parsed)) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
return parsed.filter((pid): pid is number => Number.isSafeInteger(pid) && pid > 0);
|
|
79
|
+
} catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private persist(): void {
|
|
85
|
+
try {
|
|
86
|
+
fs.mkdirSync(path.dirname(this.filePath), { recursive: true, mode: 0o700 });
|
|
87
|
+
// Write exactly the live set. Unioning with the previous file would
|
|
88
|
+
// resurrect exited pids, and a later host reaping them could kill an
|
|
89
|
+
// unrelated process group that reused one.
|
|
90
|
+
fs.writeFileSync(this.filePath, `${JSON.stringify([...this.pids])}\n`, {
|
|
91
|
+
encoding: "utf8",
|
|
92
|
+
mode: 0o600,
|
|
93
|
+
});
|
|
94
|
+
} catch {
|
|
95
|
+
// Registry bookkeeping is best-effort; orphan reaping is the backstop.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isAlive(pid: number): boolean {
|
|
101
|
+
try {
|
|
102
|
+
process.kill(pid, 0);
|
|
103
|
+
return true;
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function killProcessGroup(pid: number, signal: NodeJS.Signals): void {
|
|
110
|
+
try {
|
|
111
|
+
process.kill(-pid, signal);
|
|
112
|
+
} catch {
|
|
113
|
+
try {
|
|
114
|
+
process.kill(pid, signal);
|
|
115
|
+
} catch {
|
|
116
|
+
// Already gone.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
|
|
5
|
+
export const RPC_SUBMISSION_PREFIX = "PI_WORKFLOWS_STEP_SUBMISSION ";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Loaded into headless `pi --mode rpc` children spawned by the standalone
|
|
9
|
+
* host. The child has no workflow engine, so this bridge only registers the
|
|
10
|
+
* `workflow` tool and reports every submission to the host over stderr; the
|
|
11
|
+
* host validates against the engine and re-prompts on rejection.
|
|
12
|
+
*/
|
|
13
|
+
export default function piWorkflowsRpcBridge(pi: ExtensionAPI) {
|
|
14
|
+
pi.registerTool({
|
|
15
|
+
name: "workflow",
|
|
16
|
+
label: "Workflow",
|
|
17
|
+
description: [
|
|
18
|
+
"Submit the output for the pending workflow step with action submit.",
|
|
19
|
+
"Only call this tool when a workflow step contract in the conversation asks you to.",
|
|
20
|
+
"Pass the exact step id from the contract and your result as the output.",
|
|
21
|
+
].join(" "),
|
|
22
|
+
parameters: Type.Object(
|
|
23
|
+
{
|
|
24
|
+
action: StringEnum(["submit"] as const),
|
|
25
|
+
step: Type.String({ description: "Step id from the workflow step contract" }),
|
|
26
|
+
attempt: Type.String({ description: "Attempt id from the workflow step contract" }),
|
|
27
|
+
output: Type.Unknown({ description: "Step output matching the expected shape" }),
|
|
28
|
+
},
|
|
29
|
+
{ additionalProperties: false },
|
|
30
|
+
),
|
|
31
|
+
async execute(_toolCallId, params) {
|
|
32
|
+
process.stderr.write(`${RPC_SUBMISSION_PREFIX}${JSON.stringify(params)}\n`);
|
|
33
|
+
return {
|
|
34
|
+
content: [
|
|
35
|
+
{
|
|
36
|
+
type: "text",
|
|
37
|
+
text: "Submission recorded. Continue only when the workflow sends the next step.",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
details: {},
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|