pi-session-orchestrator 0.2.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/LICENSE +21 -0
- package/README.md +307 -0
- package/assets/pi-session-orchestrator-thumbnail-pi-sessions.png +0 -0
- package/docs/README.md +18 -0
- package/docs/pi-intercom.md +7 -0
- package/docs/prompt-injection.md +52 -0
- package/docs/reference/README.md +14 -0
- package/docs/reference/coordination-model.md +9 -0
- package/docs/reference/fast-decision-model.md +23 -0
- package/docs/reference/operational-boundaries.md +7 -0
- package/docs/release-version-preparation.md +55 -0
- package/package.json +58 -0
- package/src/binding.ts +50 -0
- package/src/index.ts +311 -0
- package/src/model.ts +164 -0
- package/src/prompt.ts +118 -0
- package/src/state.ts +639 -0
- package/src/tools.ts +275 -0
package/src/tools.ts
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
import { activeCanonicalBinding } from "./binding";
|
|
5
|
+
import type { AssignmentRole, DurableState } from "./model";
|
|
6
|
+
import { footerStatus, roleForState } from "./prompt";
|
|
7
|
+
import { LocalStateStore } from "./state";
|
|
8
|
+
|
|
9
|
+
const assignmentParameters = Type.Object({
|
|
10
|
+
sessionId: Type.String({ minLength: 1 }),
|
|
11
|
+
role: Type.Union([Type.Literal("domain-coordinator"), Type.Literal("focused-session")]),
|
|
12
|
+
objective: Type.String({ minLength: 1 }),
|
|
13
|
+
allowedScope: Type.String({ minLength: 1 }),
|
|
14
|
+
worktree: Type.Optional(Type.String({ minLength: 1 })),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const updateParameters = Type.Object({
|
|
18
|
+
operation: Type.Union([
|
|
19
|
+
Type.Literal("register-root"),
|
|
20
|
+
Type.Literal("assign-existing"),
|
|
21
|
+
Type.Literal("attach"),
|
|
22
|
+
Type.Literal("start"),
|
|
23
|
+
Type.Literal("request-decision"),
|
|
24
|
+
Type.Literal("propose-scope-change"),
|
|
25
|
+
Type.Literal("approve-scope-change"),
|
|
26
|
+
Type.Literal("approve-scope-change-human"),
|
|
27
|
+
Type.Literal("reconcile-scope-change"),
|
|
28
|
+
Type.Literal("submit-handoff"),
|
|
29
|
+
Type.Literal("settle"),
|
|
30
|
+
Type.Literal("terminal"),
|
|
31
|
+
]),
|
|
32
|
+
assignments: Type.Optional(Type.Array(assignmentParameters, { minItems: 1 })),
|
|
33
|
+
assignmentId: Type.Optional(Type.String({ minLength: 1 })),
|
|
34
|
+
proposalId: Type.Optional(Type.String({ minLength: 1 })),
|
|
35
|
+
reconciliationId: Type.Optional(Type.String({ minLength: 1 })),
|
|
36
|
+
scopeRevision: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
37
|
+
requestedScope: Type.Optional(Type.String({ minLength: 1 })),
|
|
38
|
+
evidence: Type.Optional(Type.Array(Type.String())),
|
|
39
|
+
summary: Type.Optional(Type.String({ minLength: 1 })),
|
|
40
|
+
details: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
41
|
+
outcome: Type.Optional(Type.Union([
|
|
42
|
+
Type.Literal("accepted"),
|
|
43
|
+
Type.Literal("returned"),
|
|
44
|
+
Type.Literal("blocked"),
|
|
45
|
+
Type.Literal("abandoned"),
|
|
46
|
+
])),
|
|
47
|
+
reason: Type.Optional(Type.String({ minLength: 1 })),
|
|
48
|
+
artifactIdentity: Type.Optional(Type.String()),
|
|
49
|
+
changedSurfaces: Type.Optional(Type.Array(Type.String())),
|
|
50
|
+
checks: Type.Optional(Type.Array(Type.String())),
|
|
51
|
+
risks: Type.Optional(Type.Array(Type.String())),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
type ToolContext = Pick<ExtensionContext, "cwd" | "sessionManager" | "ui" | "mode" | "hasUI">;
|
|
55
|
+
export type OrchestrationEventPresenter = (ctx: ToolContext) => void;
|
|
56
|
+
type AssignmentInput = {
|
|
57
|
+
sessionId: string;
|
|
58
|
+
role: AssignmentRole;
|
|
59
|
+
objective: string;
|
|
60
|
+
allowedScope: string;
|
|
61
|
+
worktree?: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type UpdateInput = {
|
|
65
|
+
operation: "register-root" | "assign-existing" | "attach" | "start" | "request-decision" | "propose-scope-change" | "approve-scope-change" | "approve-scope-change-human" | "reconcile-scope-change" | "submit-handoff" | "settle" | "terminal";
|
|
66
|
+
assignments?: AssignmentInput[];
|
|
67
|
+
assignmentId?: string;
|
|
68
|
+
proposalId?: string;
|
|
69
|
+
reconciliationId?: string;
|
|
70
|
+
scopeRevision?: number;
|
|
71
|
+
requestedScope?: string;
|
|
72
|
+
evidence?: string[];
|
|
73
|
+
summary?: string;
|
|
74
|
+
details?: Record<string, unknown>;
|
|
75
|
+
outcome?: "accepted" | "returned" | "blocked" | "abandoned";
|
|
76
|
+
reason?: string;
|
|
77
|
+
artifactIdentity?: string;
|
|
78
|
+
changedSurfaces?: string[];
|
|
79
|
+
checks?: string[];
|
|
80
|
+
risks?: string[];
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
84
|
+
|
|
85
|
+
export function isCanonicalSessionId(value: string): boolean {
|
|
86
|
+
return SESSION_ID.test(value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function requireValue<T>(value: T | undefined, field: string): T {
|
|
90
|
+
if (value === undefined || value === "") throw new Error(`${field} is required for this operation`);
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function compactAssignment(assignment: DurableState["assignments"][number]) {
|
|
95
|
+
return {
|
|
96
|
+
id: assignment.id,
|
|
97
|
+
sessionId: assignment.focusedSessionId,
|
|
98
|
+
role: assignment.targetRole ?? "focused-session",
|
|
99
|
+
objective: assignment.objective,
|
|
100
|
+
allowedScope: assignment.allowedScope,
|
|
101
|
+
scopeRevision: assignment.scopeRevision ?? 1,
|
|
102
|
+
status: assignment.status,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function snapshot(store: LocalStateStore, sessionId: string) {
|
|
107
|
+
const state = store.read();
|
|
108
|
+
const role = roleForState(state, sessionId);
|
|
109
|
+
const pendingAttachments = state.assignments
|
|
110
|
+
.filter((assignment) => assignment.focusedSessionId === sessionId && assignment.status === "created")
|
|
111
|
+
.map(compactAssignment);
|
|
112
|
+
const directAssignments = state.assignments
|
|
113
|
+
.filter((assignment) => assignment.coordinatorSessionId === sessionId && assignment.status !== "accepted" && assignment.status !== "returned")
|
|
114
|
+
.slice(0, 12)
|
|
115
|
+
.map((assignment) => ({ ...compactAssignment(assignment), presence: store.presenceFor(assignment.focusedSessionId) }));
|
|
116
|
+
const assignmentIds = new Set(state.assignments
|
|
117
|
+
.filter((assignment) => assignment.coordinatorSessionId === sessionId || assignment.focusedSessionId === sessionId)
|
|
118
|
+
.map((assignment) => assignment.id));
|
|
119
|
+
const scopeChanges = state.scopeChanges ?? { version: 1 as const, proposals: [], reconciliations: [] };
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
sessionId,
|
|
123
|
+
role: role.kind,
|
|
124
|
+
presence: store.presenceFor(sessionId),
|
|
125
|
+
footer: footerStatus(state, sessionId),
|
|
126
|
+
pendingAttachments,
|
|
127
|
+
directAssignments,
|
|
128
|
+
scope: {
|
|
129
|
+
assignments: state.assignments.filter((assignment) => assignmentIds.has(assignment.id)).map(compactAssignment),
|
|
130
|
+
proposals: scopeChanges.proposals.filter((proposal) => assignmentIds.has(proposal.assignmentId)),
|
|
131
|
+
reconciliations: scopeChanges.reconciliations.filter((reconciliation) => assignmentIds.has(reconciliation.assignmentId)),
|
|
132
|
+
},
|
|
133
|
+
nextPromptContext: "Applies on the next model run after a successful state transition.",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function response(store: LocalStateStore, sessionId: string, message: string) {
|
|
138
|
+
const state = snapshot(store, sessionId);
|
|
139
|
+
return {
|
|
140
|
+
content: [{ type: "text" as const, text: `${message}\n${JSON.stringify(state)}` }],
|
|
141
|
+
details: state,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function refreshFooter(store: LocalStateStore, ctx: ToolContext): void {
|
|
146
|
+
ctx.ui.setStatus("pi-session-orchestrator", footerStatus(store.read(), ctx.sessionManager.getSessionId()));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function update(store: LocalStateStore, params: UpdateInput, ctx: ToolContext): string {
|
|
150
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
151
|
+
|
|
152
|
+
switch (params.operation) {
|
|
153
|
+
case "register-root":
|
|
154
|
+
store.registerCoordinator(sessionId);
|
|
155
|
+
return "Registered the current session as a root coordinator.";
|
|
156
|
+
case "assign-existing": {
|
|
157
|
+
const assignments = requireValue(params.assignments, "assignments");
|
|
158
|
+
if (assignments.some((assignment) => !isCanonicalSessionId(assignment.sessionId))) throw new Error("assign-existing requires full canonical Pi session IDs");
|
|
159
|
+
const created = store.createAssignments(assignments.map((assignment) => ({
|
|
160
|
+
coordinatorSessionId: sessionId,
|
|
161
|
+
focusedSessionId: assignment.sessionId,
|
|
162
|
+
targetRole: assignment.role,
|
|
163
|
+
binding: activeCanonicalBinding(assignment.worktree ?? ctx.cwd),
|
|
164
|
+
objective: assignment.objective,
|
|
165
|
+
allowedScope: assignment.allowedScope,
|
|
166
|
+
})));
|
|
167
|
+
return `Created ${created.length} pending assignment${created.length === 1 ? "" : "s"}; each target must attach explicitly.`;
|
|
168
|
+
}
|
|
169
|
+
case "attach":
|
|
170
|
+
store.attach(requireValue(params.assignmentId, "assignmentId"), sessionId, activeCanonicalBinding(ctx.cwd));
|
|
171
|
+
return "Attached the current session to its validated assignment.";
|
|
172
|
+
case "start":
|
|
173
|
+
store.activate(sessionId);
|
|
174
|
+
return "Marked the current assignment active.";
|
|
175
|
+
case "request-decision":
|
|
176
|
+
store.decide(sessionId, requireValue(params.summary, "summary"), params.details ?? {});
|
|
177
|
+
return "Recorded a coordinator decision request.";
|
|
178
|
+
case "propose-scope-change":
|
|
179
|
+
store.proposeScopeChange(sessionId, {
|
|
180
|
+
baseScopeRevision: requireValue(params.scopeRevision, "scopeRevision"),
|
|
181
|
+
requestedScope: requireValue(params.requestedScope, "requestedScope"),
|
|
182
|
+
reason: requireValue(params.reason, "reason"),
|
|
183
|
+
evidence: params.evidence ?? [],
|
|
184
|
+
});
|
|
185
|
+
return "Recorded a scope-change proposal against the current assignment revision.";
|
|
186
|
+
case "approve-scope-change":
|
|
187
|
+
store.approveScopeChange(sessionId, requireValue(params.assignmentId, "assignmentId"), requireValue(params.proposalId, "proposalId"), requireValue(params.scopeRevision, "scopeRevision"));
|
|
188
|
+
return "Approved the scope change and reissued the assignment at the next scope revision.";
|
|
189
|
+
case "approve-scope-change-human":
|
|
190
|
+
store.approveScopeChangeAsHuman(sessionId, requireValue(params.proposalId, "proposalId"), requireValue(params.scopeRevision, "scopeRevision"));
|
|
191
|
+
return "Recorded direct human scope approval; coordinator reconciliation is now required.";
|
|
192
|
+
case "reconcile-scope-change":
|
|
193
|
+
store.reconcileScopeChange(sessionId, requireValue(params.reconciliationId, "reconciliationId"), requireValue(params.scopeRevision, "scopeRevision"));
|
|
194
|
+
return "Reconciled the direct-human scope change without a second approval.";
|
|
195
|
+
case "submit-handoff":
|
|
196
|
+
store.submitHandoff(sessionId, {
|
|
197
|
+
outcome: requireValue(params.summary, "summary"),
|
|
198
|
+
artifactIdentity: params.artifactIdentity,
|
|
199
|
+
changedSurfaces: params.changedSurfaces ?? [],
|
|
200
|
+
checks: params.checks ?? [],
|
|
201
|
+
risks: params.risks ?? [],
|
|
202
|
+
});
|
|
203
|
+
return "Recorded a handoff; it requires explicit coordinator settlement and does not authorize delivery.";
|
|
204
|
+
case "settle": {
|
|
205
|
+
const outcome = requireValue(params.outcome, "outcome");
|
|
206
|
+
if (outcome !== "accepted" && outcome !== "returned") throw new Error("settle requires outcome accepted or returned");
|
|
207
|
+
store.acceptOrReturn(sessionId, requireValue(params.assignmentId, "assignmentId"), outcome, params.reason);
|
|
208
|
+
return `Marked the handoff ${outcome}.`;
|
|
209
|
+
}
|
|
210
|
+
case "terminal": {
|
|
211
|
+
const outcome = requireValue(params.outcome, "outcome");
|
|
212
|
+
if (outcome !== "blocked" && outcome !== "abandoned") throw new Error("terminal requires outcome blocked or abandoned");
|
|
213
|
+
store.markTerminal(sessionId, outcome, requireValue(params.reason, "reason"));
|
|
214
|
+
return `Marked the current assignment ${outcome}.`;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Registers the narrow agent-facing control plane; session creation remains external. */
|
|
220
|
+
export function registerOrchestratorTools(pi: ExtensionAPI, store: LocalStateStore,
|
|
221
|
+
present?: OrchestrationEventPresenter): void {
|
|
222
|
+
pi.registerTool({
|
|
223
|
+
name: "orchestrator_status",
|
|
224
|
+
label: "Orchestrator Status",
|
|
225
|
+
description: "Read the current session's durable orchestration role, pending attachment, direct assignments, footer, and next prompt-context state.",
|
|
226
|
+
promptSnippet: "Inspect explicit managed-session coordination state.",
|
|
227
|
+
promptGuidelines: ["Use orchestrator_status before creating or changing managed session coordination state."],
|
|
228
|
+
parameters: Type.Object({}),
|
|
229
|
+
executionMode: "sequential",
|
|
230
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
231
|
+
store.observePresence(ctx.sessionManager.getSessionId(), "live");
|
|
232
|
+
present?.(ctx);
|
|
233
|
+
return response(store, ctx.sessionManager.getSessionId(), "Read current orchestration state.");
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
pi.registerTool({
|
|
238
|
+
name: "orchestrator_update",
|
|
239
|
+
label: "Orchestrator Update",
|
|
240
|
+
description: "Explicitly register, assign, attach, transition, hand off, settle, or end durable managed-session coordination state. It never creates sessions, worktrees, branches, or delivery actions.",
|
|
241
|
+
promptSnippet: "Explicitly update managed-session coordination state.",
|
|
242
|
+
promptGuidelines: ["Use orchestrator_update only for explicit coordination transitions; never infer a role from conversation, repository topology, or session creation."],
|
|
243
|
+
parameters: updateParameters,
|
|
244
|
+
executionMode: "sequential",
|
|
245
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
246
|
+
try {
|
|
247
|
+
store.observePresence(ctx.sessionManager.getSessionId(), "live", undefined, false);
|
|
248
|
+
const message = update(store, params as UpdateInput, ctx);
|
|
249
|
+
present?.(ctx);
|
|
250
|
+
refreshFooter(store, ctx);
|
|
251
|
+
return response(store, ctx.sessionManager.getSessionId(), message);
|
|
252
|
+
} catch (error: unknown) {
|
|
253
|
+
const message = error instanceof Error ? error.message : "Orchestration update failed";
|
|
254
|
+
return {
|
|
255
|
+
content: [{ type: "text" as const, text: message }],
|
|
256
|
+
details: snapshot(store, ctx.sessionManager.getSessionId()),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
pi.registerTool({
|
|
263
|
+
name: "orchestrator_ledger",
|
|
264
|
+
label: "Orchestrator Ledger",
|
|
265
|
+
description: "Read a bounded per-root workflow ledger derived from typed assignments, events, handoffs, and presence. It never copies Pi or intercom transcripts.",
|
|
266
|
+
promptSnippet: "Inspect a compact derived workflow ledger.",
|
|
267
|
+
promptGuidelines: ["Use orchestrator_ledger for explicit read-only coordination analysis, not for ordinary status checks."],
|
|
268
|
+
parameters: Type.Object({ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })) }),
|
|
269
|
+
executionMode: "sequential",
|
|
270
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
271
|
+
const ledger = store.workflowLedger(ctx.sessionManager.getSessionId(), params.limit ?? 20);
|
|
272
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(ledger) }], details: ledger };
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|