killeros 2.0.9 → 2.0.11
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/CHANGELOG.md +18 -0
- package/Killeros.ts +5 -22
- package/README.md +21 -20
- package/killeros/activity.ts +1 -1
- package/killeros/auto-compaction.ts +224 -0
- package/killeros/commands.ts +3 -3
- package/killeros/display.ts +2 -2
- package/killeros/footer.ts +2 -2
- package/killeros/goals.ts +79 -6
- package/killeros/init-evidence.ts +5 -5
- package/killeros/init.ts +1 -1
- package/killeros/notifications.ts +6 -36
- package/killeros/question.ts +7 -19
- package/killeros/runtime.ts +2 -0
- package/killeros/settings.ts +47 -0
- package/killeros/variants.ts +2 -2
- package/package.json +5 -5
- package/killeros/decision-gated-workflow.ts +0 -76
- package/killeros/workflow-gate.ts +0 -347
|
@@ -1,347 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExtensionAPI,
|
|
3
|
-
ExtensionContext,
|
|
4
|
-
ToolCallEvent,
|
|
5
|
-
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import type { QuestionDetails, QuestionParamsValue, QuestionRunner } from "./question.ts";
|
|
7
|
-
|
|
8
|
-
export type WorkflowToolAuthorization = true | false | string;
|
|
9
|
-
|
|
10
|
-
export interface WorkflowPolicy {
|
|
11
|
-
id: string;
|
|
12
|
-
allowedTools: readonly string[];
|
|
13
|
-
authorizeTool?: (
|
|
14
|
-
toolName: string,
|
|
15
|
-
input: Readonly<Record<string, unknown>>,
|
|
16
|
-
ctx: ExtensionContext,
|
|
17
|
-
) => WorkflowToolAuthorization;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface WorkflowAdapter {
|
|
21
|
-
id: string;
|
|
22
|
-
activation: string;
|
|
23
|
-
question: QuestionParamsValue;
|
|
24
|
-
policies: readonly WorkflowPolicy[];
|
|
25
|
-
selectPolicy: (details: QuestionDetails) => WorkflowPolicy | undefined;
|
|
26
|
-
onActivated?: (policy: WorkflowPolicy, details: QuestionDetails) => void | Promise<void>;
|
|
27
|
-
onFinish?: () => void | Promise<void>;
|
|
28
|
-
onCancel?: (reason: string) => void | Promise<void>;
|
|
29
|
-
onFailure?: (error: unknown) => void | Promise<void>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export type WorkflowGateState =
|
|
33
|
-
| { kind: "inactive" }
|
|
34
|
-
| { kind: "pending_decision"; adapterId: string; activation: string }
|
|
35
|
-
| { kind: "active"; adapterId: string; activation: string; policyId: string }
|
|
36
|
-
| { kind: "terminal_cleanup"; adapterId: string; activation: string; reason: WorkflowTerminalReason };
|
|
37
|
-
|
|
38
|
-
export interface WorkflowGateController {
|
|
39
|
-
getState(): WorkflowGateState;
|
|
40
|
-
finish(): Promise<boolean>;
|
|
41
|
-
cancel(reason?: string): Promise<boolean>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export type WorkflowTerminalReason = "finish" | "cancel" | "fail" | "session-reset";
|
|
45
|
-
|
|
46
|
-
type InternalState =
|
|
47
|
-
| { kind: "inactive" }
|
|
48
|
-
| {
|
|
49
|
-
kind: "pending_decision";
|
|
50
|
-
adapter: WorkflowAdapter;
|
|
51
|
-
abortController: AbortController;
|
|
52
|
-
token: symbol;
|
|
53
|
-
}
|
|
54
|
-
| { kind: "active"; adapter: WorkflowAdapter; policy: WorkflowPolicy; token: symbol }
|
|
55
|
-
| { kind: "terminal_cleanup"; adapter: WorkflowAdapter; reason: WorkflowTerminalReason; token: symbol };
|
|
56
|
-
|
|
57
|
-
function explicitSkillActivation(text: string): string | undefined {
|
|
58
|
-
if (!text.startsWith("/skill:")) return;
|
|
59
|
-
const spaceIndex = text.indexOf(" ");
|
|
60
|
-
const activation = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);
|
|
61
|
-
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(activation) ? activation : undefined;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function isCancelled(details: QuestionDetails): boolean {
|
|
65
|
-
if (details.cancelled) return true;
|
|
66
|
-
return "answer" in details && details.answer === null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function errorMessage(error: unknown): string {
|
|
70
|
-
return error instanceof Error ? error.message : String(error);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function publicState(state: InternalState): WorkflowGateState {
|
|
74
|
-
if (state.kind === "inactive") return state;
|
|
75
|
-
if (state.kind === "pending_decision") {
|
|
76
|
-
return {
|
|
77
|
-
kind: state.kind,
|
|
78
|
-
adapterId: state.adapter.id,
|
|
79
|
-
activation: state.adapter.activation,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
if (state.kind === "terminal_cleanup") {
|
|
83
|
-
return {
|
|
84
|
-
kind: state.kind,
|
|
85
|
-
adapterId: state.adapter.id,
|
|
86
|
-
activation: state.adapter.activation,
|
|
87
|
-
reason: state.reason,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
return {
|
|
91
|
-
kind: state.kind,
|
|
92
|
-
adapterId: state.adapter.id,
|
|
93
|
-
activation: state.adapter.activation,
|
|
94
|
-
policyId: state.policy.id,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function notify(ctx: ExtensionContext, message: string, type: "info" | "warning" | "error" = "error"): void {
|
|
99
|
-
ctx.ui.notify(message, type);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function isKnownPolicy(adapter: WorkflowAdapter, policy: WorkflowPolicy): boolean {
|
|
103
|
-
return adapter.policies.includes(policy);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function invokeCleanup(callback: (() => void | Promise<void>) | undefined): Promise<void> {
|
|
107
|
-
try {
|
|
108
|
-
await callback?.();
|
|
109
|
-
} catch {
|
|
110
|
-
// Cleanup callbacks are best effort; the gate must still reach a terminal state.
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async function invokeCancel(
|
|
115
|
-
callback: ((reason: string) => void | Promise<void>) | undefined,
|
|
116
|
-
reason: string,
|
|
117
|
-
): Promise<void> {
|
|
118
|
-
try {
|
|
119
|
-
await callback?.(reason);
|
|
120
|
-
} catch {
|
|
121
|
-
// Cleanup callbacks are best effort; the gate must still reach a terminal state.
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function invokeFailure(
|
|
126
|
-
callback: ((error: unknown) => void | Promise<void>) | undefined,
|
|
127
|
-
error: unknown,
|
|
128
|
-
): Promise<void> {
|
|
129
|
-
try {
|
|
130
|
-
await callback?.(error);
|
|
131
|
-
} catch {
|
|
132
|
-
// Cleanup callbacks are best effort; the gate must still reach a terminal state.
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function registerWorkflowGate(
|
|
137
|
-
pi: ExtensionAPI,
|
|
138
|
-
questionRunner: QuestionRunner,
|
|
139
|
-
adapters: readonly WorkflowAdapter[],
|
|
140
|
-
): WorkflowGateController {
|
|
141
|
-
const adaptersByActivation = new Map<string, WorkflowAdapter>();
|
|
142
|
-
for (const adapter of adapters) {
|
|
143
|
-
if (!adapter.id.trim()) throw new Error("Decision-gated workflow adapters require an id");
|
|
144
|
-
if (!/^[-A-Za-z0-9._]+$/u.test(adapter.activation)) {
|
|
145
|
-
throw new Error(`Invalid decision-gated workflow activation: ${adapter.activation}`);
|
|
146
|
-
}
|
|
147
|
-
if (adaptersByActivation.has(adapter.activation)) {
|
|
148
|
-
throw new Error(`Duplicate decision-gated workflow activation: ${adapter.activation}`);
|
|
149
|
-
}
|
|
150
|
-
if (adapter.policies.length === 0) throw new Error(`Workflow adapter ${adapter.id} has no policies`);
|
|
151
|
-
adaptersByActivation.set(adapter.activation, adapter);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
let state: InternalState = { kind: "inactive" };
|
|
155
|
-
|
|
156
|
-
const transitionToCleanup = (
|
|
157
|
-
current: Exclude<InternalState, { kind: "inactive" } | { kind: "terminal_cleanup" }>,
|
|
158
|
-
reason: WorkflowTerminalReason,
|
|
159
|
-
): symbol => {
|
|
160
|
-
state = {
|
|
161
|
-
kind: "terminal_cleanup",
|
|
162
|
-
adapter: current.adapter,
|
|
163
|
-
reason,
|
|
164
|
-
token: current.token,
|
|
165
|
-
};
|
|
166
|
-
if (current.kind === "pending_decision") current.abortController.abort();
|
|
167
|
-
return current.token;
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const finishCleanup = (token: symbol): void => {
|
|
171
|
-
if (state.kind === "terminal_cleanup" && state.token === token) state = { kind: "inactive" };
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const finish = async (): Promise<boolean> => {
|
|
175
|
-
if (state.kind !== "active") return false;
|
|
176
|
-
const active = state;
|
|
177
|
-
const token = transitionToCleanup(active, "finish");
|
|
178
|
-
await invokeCleanup(active.adapter.onFinish);
|
|
179
|
-
finishCleanup(token);
|
|
180
|
-
return true;
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
const cancel = async (reason = "Workflow cancelled"): Promise<boolean> => {
|
|
184
|
-
if (state.kind === "inactive" || state.kind === "terminal_cleanup") return false;
|
|
185
|
-
const current = state;
|
|
186
|
-
const token = transitionToCleanup(current, "cancel");
|
|
187
|
-
await invokeCancel(current.adapter.onCancel, reason);
|
|
188
|
-
finishCleanup(token);
|
|
189
|
-
return true;
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
const fail = async (
|
|
193
|
-
adapter: WorkflowAdapter,
|
|
194
|
-
token: symbol,
|
|
195
|
-
error: unknown,
|
|
196
|
-
ctx: ExtensionContext,
|
|
197
|
-
): Promise<void> => {
|
|
198
|
-
if (state.kind === "inactive" || state.kind === "terminal_cleanup") return;
|
|
199
|
-
if (state.adapter !== adapter || state.token !== token) return;
|
|
200
|
-
const cleanupToken = transitionToCleanup(state, "fail");
|
|
201
|
-
await invokeFailure(adapter.onFailure, error);
|
|
202
|
-
finishCleanup(cleanupToken);
|
|
203
|
-
notify(ctx, `Decision-gated workflow was not activated: ${errorMessage(error)}`);
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
pi.on("input", async (event, ctx) => {
|
|
207
|
-
const activation = explicitSkillActivation(event.text);
|
|
208
|
-
if (!activation) return;
|
|
209
|
-
|
|
210
|
-
if (state.kind === "pending_decision" || state.kind === "terminal_cleanup") {
|
|
211
|
-
notify(ctx, "A decision-gated workflow is waiting for its structured question; skill routing is blocked", "warning");
|
|
212
|
-
return { action: "handled" };
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const adapter = adaptersByActivation.get(activation);
|
|
216
|
-
if (!adapter) return;
|
|
217
|
-
|
|
218
|
-
if (state.kind === "active") {
|
|
219
|
-
notify(ctx, `Cannot start /skill:${activation} while a decision-gated workflow is active`, "warning");
|
|
220
|
-
return { action: "handled" };
|
|
221
|
-
}
|
|
222
|
-
if (ctx.mode !== "tui" || !ctx.hasUI) {
|
|
223
|
-
notify(ctx, `The /skill:${activation} workflow requires interactive TUI mode`);
|
|
224
|
-
return { action: "handled" };
|
|
225
|
-
}
|
|
226
|
-
if (!ctx.isIdle() || ctx.hasPendingMessages()) {
|
|
227
|
-
notify(ctx, `The /skill:${activation} workflow can start only when Pi is idle`, "warning");
|
|
228
|
-
return { action: "handled" };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const abortController = new AbortController();
|
|
232
|
-
const token = Symbol();
|
|
233
|
-
state = { kind: "pending_decision", adapter, abortController, token };
|
|
234
|
-
let details: QuestionDetails;
|
|
235
|
-
try {
|
|
236
|
-
details = await questionRunner.ask(adapter.question, abortController.signal, ctx);
|
|
237
|
-
} catch (error) {
|
|
238
|
-
if (state.kind !== "pending_decision" || state.adapter !== adapter || state.token !== token) {
|
|
239
|
-
return { action: "handled" };
|
|
240
|
-
}
|
|
241
|
-
await fail(adapter, token, error, ctx);
|
|
242
|
-
return { action: "handled" };
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (state.kind !== "pending_decision" || state.adapter !== adapter || state.token !== token) {
|
|
246
|
-
return { action: "handled" };
|
|
247
|
-
}
|
|
248
|
-
if (isCancelled(details)) {
|
|
249
|
-
await cancel("Decision question cancelled");
|
|
250
|
-
notify(ctx, `The /skill:${activation} workflow was cancelled`, "warning");
|
|
251
|
-
return { action: "handled" };
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
let policy: WorkflowPolicy | undefined;
|
|
255
|
-
try {
|
|
256
|
-
policy = adapter.selectPolicy(details);
|
|
257
|
-
} catch (error) {
|
|
258
|
-
await fail(adapter, token, error, ctx);
|
|
259
|
-
return { action: "handled" };
|
|
260
|
-
}
|
|
261
|
-
if (!policy || !isKnownPolicy(adapter, policy)) {
|
|
262
|
-
await fail(adapter, token, new Error("The structured answer did not select a registered policy"), ctx);
|
|
263
|
-
return { action: "handled" };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
try {
|
|
267
|
-
await adapter.onActivated?.(policy, details);
|
|
268
|
-
} catch (error) {
|
|
269
|
-
if (state.kind !== "pending_decision" || state.adapter !== adapter || state.token !== token) {
|
|
270
|
-
return { action: "handled" };
|
|
271
|
-
}
|
|
272
|
-
await fail(adapter, token, error, ctx);
|
|
273
|
-
return { action: "handled" };
|
|
274
|
-
}
|
|
275
|
-
if (state.kind !== "pending_decision" || state.adapter !== adapter || state.token !== token) {
|
|
276
|
-
return { action: "handled" };
|
|
277
|
-
}
|
|
278
|
-
state = { kind: "active", adapter, policy, token };
|
|
279
|
-
return { action: "continue" };
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
pi.on("tool_call", (event: ToolCallEvent, ctx) => {
|
|
283
|
-
if (state.kind === "inactive") return;
|
|
284
|
-
if (state.kind === "pending_decision" || state.kind === "terminal_cleanup") {
|
|
285
|
-
return {
|
|
286
|
-
block: true,
|
|
287
|
-
reason: "A decision-gated workflow is waiting for its structured question; no model tool calls are allowed yet",
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const { policy } = state;
|
|
292
|
-
if (!policy.allowedTools.includes(event.toolName)) {
|
|
293
|
-
return {
|
|
294
|
-
block: true,
|
|
295
|
-
reason: `Tool ${event.toolName} is not allowed by decision-gated policy ${policy.id}`,
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
const authorization = policy.authorizeTool?.(event.toolName, event.input, ctx) ?? true;
|
|
299
|
-
if (authorization === true) return;
|
|
300
|
-
return {
|
|
301
|
-
block: true,
|
|
302
|
-
reason: typeof authorization === "string"
|
|
303
|
-
? authorization
|
|
304
|
-
: `Tool ${event.toolName} is denied by decision-gated policy ${policy.id}`,
|
|
305
|
-
};
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
const resetForLifecycle = async (reason: string): Promise<void> => {
|
|
309
|
-
if (state.kind === "inactive" || state.kind === "terminal_cleanup") return;
|
|
310
|
-
const current = state;
|
|
311
|
-
const token = transitionToCleanup(current, "session-reset");
|
|
312
|
-
await invokeCancel(current.adapter.onCancel, reason);
|
|
313
|
-
finishCleanup(token);
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
pi.on("session_start", () => resetForLifecycle("Session lifecycle reset"));
|
|
317
|
-
pi.on("session_shutdown", () => resetForLifecycle("Session lifecycle reset"));
|
|
318
|
-
pi.on("session_tree", () => resetForLifecycle("Session tree reset"));
|
|
319
|
-
pi.on("session_before_switch", () => resetForLifecycle("Session switch reset"));
|
|
320
|
-
pi.on("session_before_fork", () => resetForLifecycle("Session fork reset"));
|
|
321
|
-
pi.on("session_before_tree", () => resetForLifecycle("Session tree reset"));
|
|
322
|
-
|
|
323
|
-
pi.on("agent_end", async (event, ctx) => {
|
|
324
|
-
if (state.kind !== "active") return;
|
|
325
|
-
|
|
326
|
-
let failureMessage: string | undefined;
|
|
327
|
-
for (let index = event.messages.length - 1; index >= 0; index -= 1) {
|
|
328
|
-
const message = event.messages[index];
|
|
329
|
-
if (message?.role === "assistant" && message.errorMessage) {
|
|
330
|
-
failureMessage = message.errorMessage;
|
|
331
|
-
break;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
if (failureMessage === undefined) return;
|
|
335
|
-
|
|
336
|
-
const active = state;
|
|
337
|
-
await fail(active.adapter, active.token, new Error(failureMessage), ctx);
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
return {
|
|
341
|
-
getState: () => publicState(state),
|
|
342
|
-
finish,
|
|
343
|
-
cancel,
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
export { explicitSkillActivation };
|