@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/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: { action?: string; request?: unknown; resultId?: string },
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
- params,
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, params, ctx, signal);
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
- ? { ...result, [ACTIVITY_DETAILS_KEY]: display }
155
- : result;
156
- if (result.ok) {
157
- return {
158
- content: [{ type: "text", text: JSON.stringify(result) }],
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(result) }],
190
+ content: [{ type: "text", text: JSON.stringify(payload) }],
165
191
  details,
166
- isError: true,
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) => {
@@ -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<{ modelRuntime: ModelRuntime; model: Model<string> }> {
141
- signal?.throwIfAborted();
142
- if (!ctx.model) throw new Error("parent model is unavailable");
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
- const resolved = await abortable(
146
- ctx.modelRegistry.getApiKeyAndHeaders(selectedModel),
147
- signal,
148
- );
149
- if (!resolved.ok) throw new Error(resolved.error);
150
- signal?.throwIfAborted();
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) throw new Error("parent payload bridge is unavailable");
158
- const requireReadyCapture = () => {
159
- if (
160
- !ctx.model ||
161
- !sameSelectedModel(ctx.model, selectedModelKey) ||
162
- payloadBridge.capture(effectiveModelKey, ctx.modelRegistry) !== capture
163
- ) {
164
- throw new Error("parent payload bridge is unavailable");
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
- requireReadyCapture();
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
- const modelRuntime = await runtimeForProvider(provider, signal);
175
- signal?.throwIfAborted();
176
- requireReadyCapture();
177
- return { modelRuntime, model };
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> {