@abelxiaoxing/cadence 1.0.2 → 1.0.4
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/agents/contract-reviewer.md +2 -2
- package/agents/diagnosis-worker.md +3 -2
- package/agents/implementation-worker.md +3 -2
- package/config/.env.example +16 -1
- package/package.json +1 -1
- package/prompts/abel-design.md +13 -3
- package/prompts/abel-implement.md +50 -21
- package/skills/_shared/load-config.d.mts +7 -0
- package/skills/abel-workflow/SKILL.md +57 -25
- package/src/candidate-preflight.ts +361 -56
- package/src/child-session.ts +92 -27
- package/src/contracts.ts +994 -38
- package/src/index.ts +50 -24
- package/src/parent-provider.ts +206 -23
- package/src/patch.ts +269 -41
- package/src/result-store.ts +254 -16
- package/src/runtime.ts +1336 -560
- package/src/scheduler.ts +59 -16
- package/src/subagent-activity.ts +56 -25
- package/src/subagent-endpoint.ts +158 -0
- package/src/submit-tool.ts +12 -2
- package/src/worker.ts +121 -195
package/src/index.ts
CHANGED
|
@@ -99,6 +99,14 @@ function activateDispatcher(
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
function splitUsage(result: unknown): { payload: unknown; usage?: unknown } {
|
|
103
|
+
if (typeof result !== "object" || result === null || !("usage" in result)) {
|
|
104
|
+
return { payload: result };
|
|
105
|
+
}
|
|
106
|
+
const { usage, ...payload } = result as Record<string, unknown>;
|
|
107
|
+
return { payload, usage };
|
|
108
|
+
}
|
|
109
|
+
|
|
102
110
|
export default function register(pi: ExtensionAPI): void {
|
|
103
111
|
const parentPayloadBridge = new ParentPayloadBridge();
|
|
104
112
|
const runtime = new Runtime({ parentPayloadBridge });
|
|
@@ -108,7 +116,7 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
108
116
|
name: DISPATCH_TOOL,
|
|
109
117
|
label: "Abel Dispatch",
|
|
110
118
|
description:
|
|
111
|
-
"Private Abel workflow delegation: run bounded read-only evidence or Worker phase requests, apply or discard retained results, cancel work, or finish the stage. Inactive unless an eligible Abel stage verified its invocation.",
|
|
119
|
+
"Private Abel workflow delegation: run bounded read-only evidence or Worker phase requests, apply or discard retained results, apply a parent-only stable AGENTS checkpoint, cancel work, or finish the stage. Inactive unless an eligible Abel stage verified its invocation.",
|
|
112
120
|
parameters: {
|
|
113
121
|
type: "object",
|
|
114
122
|
properties: {
|
|
@@ -121,12 +129,32 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
121
129
|
type: "string",
|
|
122
130
|
description: "Retained result id for apply/discard",
|
|
123
131
|
},
|
|
132
|
+
requestId: {
|
|
133
|
+
type: "string",
|
|
134
|
+
description: "Current Implement apply/discard operation identity",
|
|
135
|
+
},
|
|
136
|
+
rejection: {
|
|
137
|
+
type: "object",
|
|
138
|
+
description: "Typed parent rejection for Implement discard",
|
|
139
|
+
},
|
|
140
|
+
agentsCheckpoint: {
|
|
141
|
+
type: "object",
|
|
142
|
+
description:
|
|
143
|
+
"Parent-owned approved managed-block checkpoint for action=apply (mutually exclusive with resultId)",
|
|
144
|
+
},
|
|
124
145
|
},
|
|
125
146
|
required: ["action"],
|
|
126
147
|
},
|
|
127
148
|
async execute(
|
|
128
149
|
toolCallId: string,
|
|
129
|
-
params: {
|
|
150
|
+
params: {
|
|
151
|
+
action?: string;
|
|
152
|
+
request?: unknown;
|
|
153
|
+
resultId?: string;
|
|
154
|
+
requestId?: string;
|
|
155
|
+
rejection?: unknown;
|
|
156
|
+
agentsCheckpoint?: unknown;
|
|
157
|
+
},
|
|
130
158
|
signal: AbortSignal | undefined,
|
|
131
159
|
onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
|
132
160
|
ctx: ExtensionContext,
|
|
@@ -135,10 +163,11 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
135
163
|
const validRun =
|
|
136
164
|
action === "run" && runtime.validateRequest(params.request).ok;
|
|
137
165
|
const tuiRun = ctx.mode === "tui" && validRun;
|
|
166
|
+
const { action: _action, ...operation } = params;
|
|
138
167
|
const result = tuiRun
|
|
139
168
|
? await runtime.execute(
|
|
140
169
|
action,
|
|
141
|
-
|
|
170
|
+
operation,
|
|
142
171
|
ctx,
|
|
143
172
|
signal,
|
|
144
173
|
activity.observe(
|
|
@@ -146,24 +175,21 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
146
175
|
onUpdate as ((result: unknown) => void) | undefined,
|
|
147
176
|
),
|
|
148
177
|
)
|
|
149
|
-
: await runtime.execute(action,
|
|
178
|
+
: await runtime.execute(action, operation, ctx, signal);
|
|
150
179
|
const display = tuiRun
|
|
151
180
|
? activity.finalize(toolCallId, result)
|
|
152
181
|
: undefined;
|
|
182
|
+
const { payload, usage } = splitUsage(result);
|
|
153
183
|
const details = display
|
|
154
|
-
? {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
details,
|
|
160
|
-
usage: result.usage,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
184
|
+
? {
|
|
185
|
+
...(payload as Record<string, unknown>),
|
|
186
|
+
[ACTIVITY_DETAILS_KEY]: display,
|
|
187
|
+
}
|
|
188
|
+
: payload;
|
|
163
189
|
return {
|
|
164
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
190
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
165
191
|
details,
|
|
166
|
-
|
|
192
|
+
...(usage === undefined ? {} : { usage }),
|
|
167
193
|
};
|
|
168
194
|
},
|
|
169
195
|
renderCall(args: unknown, theme: Theme, _context: unknown) {
|
|
@@ -207,7 +233,15 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
207
233
|
activateDispatcher(pi, runtime.activation, prompt, event.prompt);
|
|
208
234
|
});
|
|
209
235
|
|
|
210
|
-
pi.on("session_start", (_event, ctx) => {
|
|
236
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
237
|
+
pendingPrompt = undefined;
|
|
238
|
+
activity.detach();
|
|
239
|
+
const active = pi.getActiveTools();
|
|
240
|
+
if (active.includes(DISPATCH_TOOL)) {
|
|
241
|
+
pi.setActiveTools(deactivateTool(active, DISPATCH_TOOL));
|
|
242
|
+
}
|
|
243
|
+
await runtime.drain();
|
|
244
|
+
activity.clear();
|
|
211
245
|
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
212
246
|
if (typeof sessionId === "string") {
|
|
213
247
|
parentPayloadBridge.beginSession(sessionId);
|
|
@@ -217,15 +251,7 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
217
251
|
} else {
|
|
218
252
|
parentPayloadBridge.clear();
|
|
219
253
|
}
|
|
220
|
-
pendingPrompt = undefined;
|
|
221
|
-
activity.detach();
|
|
222
254
|
if (ctx.mode === "tui") activity.attach(ctx.ui);
|
|
223
|
-
// Restore the default: dispatch is inactive unless a verified stage
|
|
224
|
-
// reactivates it through the activation helpers.
|
|
225
|
-
const active = pi.getActiveTools();
|
|
226
|
-
if (active.includes(DISPATCH_TOOL)) {
|
|
227
|
-
pi.setActiveTools(deactivateTool(active, DISPATCH_TOOL));
|
|
228
|
-
}
|
|
229
255
|
});
|
|
230
256
|
|
|
231
257
|
pi.on("model_select", (event, ctx) => {
|
package/src/parent-provider.ts
CHANGED
|
@@ -2,22 +2,167 @@ import {
|
|
|
2
2
|
InMemoryCredentialStore,
|
|
3
3
|
type Model,
|
|
4
4
|
type Provider,
|
|
5
|
+
type StreamOptions,
|
|
5
6
|
} from "@earendil-works/pi-ai";
|
|
7
|
+
import {
|
|
8
|
+
createProvider,
|
|
9
|
+
stream as subagentStream,
|
|
10
|
+
streamSimple as subagentStreamSimple,
|
|
11
|
+
} from "@earendil-works/pi-ai/compat";
|
|
6
12
|
import {
|
|
7
13
|
type ExtensionContext,
|
|
8
14
|
ModelRuntime,
|
|
9
15
|
} from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import type { ChildFailure } from "./contracts.ts";
|
|
10
17
|
import type {
|
|
11
18
|
ParentPayloadBridge,
|
|
12
19
|
ParentPayloadCallback,
|
|
13
20
|
ParentPayloadCapture,
|
|
14
21
|
} from "./parent-payload-bridge.ts";
|
|
22
|
+
import type { SubagentEndpoint } from "./subagent-endpoint.ts";
|
|
23
|
+
|
|
24
|
+
const SUBAGENT_PROVIDER_ID = "abel-subagent";
|
|
25
|
+
|
|
26
|
+
function customStreamOptions<T extends StreamOptions>(
|
|
27
|
+
model: Model<string>,
|
|
28
|
+
options?: T,
|
|
29
|
+
): T | undefined {
|
|
30
|
+
if (model.api !== "openai-responses") return options;
|
|
31
|
+
const childOnPayload = options?.onPayload;
|
|
32
|
+
return {
|
|
33
|
+
...options,
|
|
34
|
+
onPayload: async (payload, requestModel) => {
|
|
35
|
+
const transformed = await childOnPayload?.(payload, requestModel);
|
|
36
|
+
const current = transformed === undefined ? payload : transformed;
|
|
37
|
+
if (!current || typeof current !== "object" || Array.isArray(current))
|
|
38
|
+
return current;
|
|
39
|
+
const normalized = { ...current } as Record<string, unknown>;
|
|
40
|
+
delete normalized.max_output_tokens;
|
|
41
|
+
return normalized;
|
|
42
|
+
},
|
|
43
|
+
} as T;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The compat dispatchers resolve the concrete API implementation from
|
|
47
|
+
// model.api at call time, so one pair serves all supported dialects.
|
|
48
|
+
const SUBAGENT_STREAMS = {
|
|
49
|
+
stream(
|
|
50
|
+
model: Parameters<typeof subagentStream>[0],
|
|
51
|
+
context: Parameters<typeof subagentStream>[1],
|
|
52
|
+
options?: Parameters<typeof subagentStream>[2],
|
|
53
|
+
) {
|
|
54
|
+
return subagentStream(
|
|
55
|
+
model,
|
|
56
|
+
context,
|
|
57
|
+
customStreamOptions(model as Model<string>, options),
|
|
58
|
+
);
|
|
59
|
+
},
|
|
60
|
+
streamSimple(
|
|
61
|
+
model: Parameters<typeof subagentStreamSimple>[0],
|
|
62
|
+
context: Parameters<typeof subagentStreamSimple>[1],
|
|
63
|
+
options?: Parameters<typeof subagentStreamSimple>[2],
|
|
64
|
+
) {
|
|
65
|
+
return subagentStreamSimple(
|
|
66
|
+
model,
|
|
67
|
+
context,
|
|
68
|
+
customStreamOptions(model as Model<string>, options),
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function customSubagentModel(endpoint: SubagentEndpoint): Model<string> {
|
|
74
|
+
return {
|
|
75
|
+
id: endpoint.model,
|
|
76
|
+
name: endpoint.model,
|
|
77
|
+
api: endpoint.dialect,
|
|
78
|
+
provider: SUBAGENT_PROVIDER_ID,
|
|
79
|
+
baseUrl: endpoint.url,
|
|
80
|
+
reasoning: true,
|
|
81
|
+
input: ["text"],
|
|
82
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
83
|
+
contextWindow: endpoint.contextWindow,
|
|
84
|
+
maxTokens: endpoint.maxTokens,
|
|
85
|
+
} as Model<string>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function customSubagentProvider(
|
|
89
|
+
endpoint: SubagentEndpoint,
|
|
90
|
+
model: Model<string>,
|
|
91
|
+
): Provider {
|
|
92
|
+
return createProvider({
|
|
93
|
+
id: SUBAGENT_PROVIDER_ID,
|
|
94
|
+
name: "Abel Subagent Endpoint",
|
|
95
|
+
baseUrl: endpoint.url,
|
|
96
|
+
auth: {
|
|
97
|
+
apiKey: {
|
|
98
|
+
name: "Abel subagent endpoint auth",
|
|
99
|
+
resolve: async () =>
|
|
100
|
+
endpoint.apiKey
|
|
101
|
+
? { auth: { apiKey: endpoint.apiKey }, source: "subagent-endpoint" }
|
|
102
|
+
: {
|
|
103
|
+
auth: {
|
|
104
|
+
apiKey: "unused",
|
|
105
|
+
headers: { authorization: null, "x-api-key": null },
|
|
106
|
+
},
|
|
107
|
+
source: "subagent-endpoint-keyless",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
models: [model],
|
|
112
|
+
api: SUBAGENT_STREAMS,
|
|
113
|
+
}) as Provider;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function customPhaseRuntime(
|
|
117
|
+
endpoint: SubagentEndpoint,
|
|
118
|
+
signal?: AbortSignal,
|
|
119
|
+
): Promise<PhaseRuntimeResult> {
|
|
120
|
+
if (signal?.aborted) return cancelledPhaseRuntime();
|
|
121
|
+
const model = customSubagentModel(endpoint);
|
|
122
|
+
const provider = customSubagentProvider(endpoint, model);
|
|
123
|
+
let modelRuntime: ModelRuntime;
|
|
124
|
+
try {
|
|
125
|
+
modelRuntime = await runtimeForProvider(provider, signal);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (isCancellation(error, signal)) return cancelledPhaseRuntime();
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
if (signal?.aborted) return cancelledPhaseRuntime();
|
|
131
|
+
return { ok: true, modelRuntime, model };
|
|
132
|
+
}
|
|
15
133
|
|
|
16
134
|
export interface PhasePayloadBridge {
|
|
17
135
|
readonly bridge: ParentPayloadBridge;
|
|
18
136
|
readonly capture: ParentPayloadCapture;
|
|
19
137
|
}
|
|
20
138
|
|
|
139
|
+
type PhaseRuntimeFailure = Extract<
|
|
140
|
+
ChildFailure,
|
|
141
|
+
{ kind: "cancelled" | "environment" | "transport" }
|
|
142
|
+
>;
|
|
143
|
+
|
|
144
|
+
export type PhaseRuntimeResult =
|
|
145
|
+
| { ok: true; modelRuntime: ModelRuntime; model: Model<string> }
|
|
146
|
+
| { ok: false; error: string; failure: PhaseRuntimeFailure };
|
|
147
|
+
|
|
148
|
+
function phaseRuntimeFailure(
|
|
149
|
+
failure: PhaseRuntimeFailure,
|
|
150
|
+
error: string,
|
|
151
|
+
): Extract<PhaseRuntimeResult, { ok: false }> {
|
|
152
|
+
return { ok: false, error, failure };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function cancelledPhaseRuntime(): Extract<PhaseRuntimeResult, { ok: false }> {
|
|
156
|
+
return phaseRuntimeFailure(
|
|
157
|
+
{ kind: "cancelled", code: "cancelled" },
|
|
158
|
+
"child phase cancelled",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isCancellation(error: unknown, signal?: AbortSignal): boolean {
|
|
163
|
+
return signal?.aborted === true && error === signal.reason;
|
|
164
|
+
}
|
|
165
|
+
|
|
21
166
|
function phasePayloadCallback(
|
|
22
167
|
payloadBridge: PhasePayloadBridge,
|
|
23
168
|
childOnPayload: ParentPayloadCallback | undefined,
|
|
@@ -137,44 +282,82 @@ export async function runtimeFromContext(
|
|
|
137
282
|
ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
|
|
138
283
|
payloadBridge: ParentPayloadBridge,
|
|
139
284
|
signal?: AbortSignal,
|
|
140
|
-
): Promise<
|
|
141
|
-
signal?.
|
|
142
|
-
if (!ctx.model)
|
|
285
|
+
): Promise<PhaseRuntimeResult> {
|
|
286
|
+
if (signal?.aborted) return cancelledPhaseRuntime();
|
|
287
|
+
if (!ctx.model) {
|
|
288
|
+
return phaseRuntimeFailure(
|
|
289
|
+
{ kind: "environment", code: "sandbox-runtime-unavailable" },
|
|
290
|
+
"parent model is unavailable",
|
|
291
|
+
);
|
|
292
|
+
}
|
|
143
293
|
const selectedModel = ctx.model;
|
|
144
294
|
const selectedModelKey = modelKeyFor(selectedModel);
|
|
145
|
-
|
|
146
|
-
ctx.modelRegistry.getApiKeyAndHeaders
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
295
|
+
let resolved: Awaited<
|
|
296
|
+
ReturnType<typeof ctx.modelRegistry.getApiKeyAndHeaders>
|
|
297
|
+
>;
|
|
298
|
+
try {
|
|
299
|
+
resolved = await abortable(
|
|
300
|
+
ctx.modelRegistry.getApiKeyAndHeaders(selectedModel),
|
|
301
|
+
signal,
|
|
302
|
+
);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (isCancellation(error, signal)) return cancelledPhaseRuntime();
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
if (!resolved.ok) {
|
|
308
|
+
return phaseRuntimeFailure(
|
|
309
|
+
{ kind: "environment", code: "sandbox-runtime-unavailable" },
|
|
310
|
+
"phase authentication is unavailable",
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (signal?.aborted) return cancelledPhaseRuntime();
|
|
151
314
|
const model = {
|
|
152
315
|
...selectedModel,
|
|
153
316
|
baseUrl: resolved.baseUrl ?? selectedModel.baseUrl,
|
|
154
317
|
} as Model<string>;
|
|
155
318
|
const effectiveModelKey = modelKeyFor(model);
|
|
156
319
|
const capture = payloadBridge.capture(effectiveModelKey, ctx.modelRegistry);
|
|
157
|
-
if (!capture)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
320
|
+
if (!capture) {
|
|
321
|
+
return phaseRuntimeFailure(
|
|
322
|
+
{ kind: "transport", code: "transport-failure" },
|
|
323
|
+
"parent payload bridge is unavailable",
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
const captureIsReady = () => {
|
|
327
|
+
const currentModel = ctx.model;
|
|
328
|
+
return (
|
|
329
|
+
currentModel !== undefined &&
|
|
330
|
+
sameSelectedModel(currentModel, selectedModelKey) &&
|
|
331
|
+
payloadBridge.capture(effectiveModelKey, ctx.modelRegistry) === capture
|
|
332
|
+
);
|
|
166
333
|
};
|
|
167
|
-
|
|
334
|
+
if (!captureIsReady()) {
|
|
335
|
+
return phaseRuntimeFailure(
|
|
336
|
+
{ kind: "transport", code: "transport-failure" },
|
|
337
|
+
"parent payload bridge is unavailable",
|
|
338
|
+
);
|
|
339
|
+
}
|
|
168
340
|
const delegate = capture.delegate;
|
|
169
341
|
if (!delegate) throw new Error("parent Provider is unavailable");
|
|
170
342
|
const provider = phaseProvider(delegate, resolved, {
|
|
171
343
|
bridge: payloadBridge,
|
|
172
344
|
capture,
|
|
173
345
|
});
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
346
|
+
let modelRuntime: ModelRuntime;
|
|
347
|
+
try {
|
|
348
|
+
modelRuntime = await runtimeForProvider(provider, signal);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
if (isCancellation(error, signal)) return cancelledPhaseRuntime();
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
if (signal?.aborted) return cancelledPhaseRuntime();
|
|
354
|
+
if (!captureIsReady()) {
|
|
355
|
+
return phaseRuntimeFailure(
|
|
356
|
+
{ kind: "transport", code: "transport-failure" },
|
|
357
|
+
"parent payload bridge is unavailable",
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
return { ok: true, modelRuntime, model };
|
|
178
361
|
}
|
|
179
362
|
|
|
180
363
|
function abortable<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
|