@abelxiaoxing/cadence 1.0.0 → 1.0.1
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 +50 -22
- package/package.json +3 -1
- package/prompts/abel-design.md +1 -0
- package/prompts/abel-diagnose.md +1 -0
- package/prompts/abel-implement.md +1 -0
- package/src/child-session.ts +53 -6
- package/src/index.ts +144 -12
- package/src/parent-provider.ts +36 -7
- package/src/runtime.ts +262 -18
- package/src/scheduler.ts +46 -10
- package/src/subagent-activity.ts +747 -0
package/README.md
CHANGED
|
@@ -1,41 +1,69 @@
|
|
|
1
1
|
# @abelxiaoxing/cadence
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@abelxiaoxing/cadence)
|
|
4
|
+
[](https://github.com/abelxiaoxing/cadence)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
- `/abel-design <requirement> | --change <change_name>` — parallelize read-only exploration and design a dependency/conflict-aware task DAG through Gate A and Gate B.
|
|
7
|
-
- `/abel-implement <change_name>` — orchestrate task-local professional Agents over the approved DAG with delegated Red-Green-Refactor and parent-owned verification.
|
|
8
|
-
- `/abel-diagnose <problem-description>` — verify a root cause before a minimal repair.
|
|
6
|
+
**Cadence** 是一个面向 pi 的规范驱动四阶段工作流扩展包,内置私有代理编排:
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
- `/abel-init [project-path]` — 初始化 OpenSpec 并修复 AGENTS 索引。
|
|
9
|
+
- `/abel-design <requirement> | --change <change_name>` — 并行只读探索,设计依赖/冲突感知的任务 DAG,经由 Gate A(行为契约)与 Gate B(技术契约)双重批准。
|
|
10
|
+
- `/abel-implement <change_name>` — 在已批准的 DAG 上编排任务级专业代理,委派 Red-Green-Refactor,由父代理执行验证。
|
|
11
|
+
- `/abel-diagnose <problem-description>` — 先验证根因,再做最小修复。
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
包是独立的:单一仓库、单一清单、单一 lockfile,无 workspace、无参考仓库检出、不依赖任何外部 Subagent 包。
|
|
14
|
+
加载本包会注册一个私有扩展与四个包内专业 Agent;`abel_dispatch` 工具注册但保持未激活,只有经过验证的 Design、Implement 或 Diagnose 阶段才会激活它。
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
共享工作流规则统一收录于包内 `abel-workflow` skill。
|
|
17
|
+
|
|
18
|
+
交互式 TUI 中,经过验证的 `abel_dispatch` run 会显示紧凑的 Subagent 角色、请求、阶段、目标、耗时和状态,并在编辑器上方临时汇总仍在排队或运行的 Agents。
|
|
19
|
+
|
|
20
|
+
该显示只保留有限的终态摘要;不会显示子会话、工具活动、路径、模型身份、隐藏推理、完整引用或完整 diff。
|
|
21
|
+
|
|
22
|
+
print、JSON 和 RPC 模式保持原有结果与事件行为不变。
|
|
23
|
+
|
|
24
|
+
## 安装(Install)
|
|
25
|
+
|
|
26
|
+
### 在 pi 中快速安装(推荐)
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
$ pi install npm:@abelxiaoxing/cadence
|
|
30
|
+
Installed npm:@abelxiaoxing/cadence
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
也可以使用 npm 或 bun:
|
|
16
34
|
|
|
17
35
|
```sh
|
|
18
36
|
npm install -g @abelxiaoxing/cadence
|
|
19
|
-
#
|
|
37
|
+
# 或使用 bun:bun add -g @abelxiaoxing/cadence
|
|
20
38
|
```
|
|
21
39
|
|
|
22
|
-
|
|
40
|
+
已验证的加载路径:
|
|
23
41
|
|
|
24
|
-
- **npm package
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
- **Installed tarball directory** — run `bun pm pack --destination <tmp>` to produce a real tarball, install or unpack it into an isolated directory, and point Pi at that directory.
|
|
28
|
-
The tarball file itself is never passed to Pi as a local package.
|
|
42
|
+
- **npm 包(npm package)** — 从 npm registry 以用户级或项目级 scope 安装 `@abelxiaoxing/cadence`;Pi 发现四个 prompts、四个 skills、私有扩展与包内 Agents。
|
|
43
|
+
- **本地包目录(local package directory)** — 将 Pi 指向本仓库的绝对路径或相对路径(absolute or relative path,例如 `./cadence`),Pi 发现同样的资源。
|
|
44
|
+
- **已安装 tarball 目录(installed tarball directory)** — 运行 `bun pm pack --destination <tmp>` 生成真实 tarball(.tgz),安装或解压到隔离目录后指向该目录;tarball 文件本身永远不会被当作本地包传给 Pi。
|
|
29
45
|
|
|
30
|
-
|
|
46
|
+
本包从不检查或分类 Pi 主机版本,不声明任何版本范围,也不从参考仓库源安装。
|
|
31
47
|
|
|
32
|
-
## Development
|
|
48
|
+
## 开发(Development)
|
|
33
49
|
|
|
34
50
|
```sh
|
|
35
51
|
bun install
|
|
36
|
-
bun run check #
|
|
52
|
+
bun run check # 语法 + 类型检查
|
|
37
53
|
bun run lint # biome + rumdl
|
|
38
|
-
bun run test #
|
|
39
|
-
bun run test:target <files> #
|
|
40
|
-
bun run check:agents # AGENTS
|
|
54
|
+
bun run test # 完整测试套件
|
|
55
|
+
bun run test:target <files> # 定向测试
|
|
56
|
+
bun run check:agents # AGENTS 索引校验
|
|
41
57
|
```
|
|
58
|
+
|
|
59
|
+
## 验证(Verification)
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
bun run verify # check && lint && test && pack:check(发布前全套校验)
|
|
63
|
+
bun run pack:check # 真实 tarball 39 成员清单校验
|
|
64
|
+
bun run traceability:check # 81 条 Requirement/Scenario 引用精确解析
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 许可(License)
|
|
68
|
+
|
|
69
|
+
MIT — 详见 [LICENSE](LICENSE) 与 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abelxiaoxing/cadence",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Prompts, skills, and a private orchestration extension exposing the Abel four-stage workflow (/abel-init, /abel-design, /abel-implement, /abel-diagnose) with package-owned professional Agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -48,11 +48,13 @@
|
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@earendil-works/pi-coding-agent": "*",
|
|
51
|
+
"@earendil-works/pi-tui": "*",
|
|
51
52
|
"typebox": "*"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
55
|
"@biomejs/biome": "^2.4.16",
|
|
55
56
|
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
57
|
+
"@earendil-works/pi-tui": "0.84.2",
|
|
56
58
|
"@types/node": "^22.15.3",
|
|
57
59
|
"rumdl": "^0.2.10",
|
|
58
60
|
"typescript": "^6.0.3",
|
package/prompts/abel-design.md
CHANGED
package/prompts/abel-diagnose.md
CHANGED
|
@@ -10,6 +10,7 @@ Read the complete value inside `<abel-request>` without a second whitespace-toke
|
|
|
10
10
|
$ARGUMENTS
|
|
11
11
|
</abel-request>
|
|
12
12
|
|
|
13
|
+
<!-- ABEL:PROMPT:abel-diagnose -->
|
|
13
14
|
The request must uniquely identify one or more existing bugs.
|
|
14
15
|
If required input is missing or ambiguous, stop and ask for the missing or disambiguating information instead of guessing.
|
|
15
16
|
|
package/src/child-session.ts
CHANGED
|
@@ -16,6 +16,20 @@ import {
|
|
|
16
16
|
type SubmitClassification,
|
|
17
17
|
} from "./submit-tool.ts";
|
|
18
18
|
|
|
19
|
+
function abortable<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
20
|
+
signal.throwIfAborted();
|
|
21
|
+
return new Promise<T>((resolve, reject) => {
|
|
22
|
+
const onAbort = () => {
|
|
23
|
+
signal.removeEventListener("abort", onAbort);
|
|
24
|
+
reject(signal.reason);
|
|
25
|
+
};
|
|
26
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
27
|
+
promise
|
|
28
|
+
.then(resolve, reject)
|
|
29
|
+
.finally(() => signal.removeEventListener("abort", onAbort));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
const ZERO_USAGE: Usage = {
|
|
20
34
|
input: 0,
|
|
21
35
|
output: 0,
|
|
@@ -78,6 +92,8 @@ function wrapScopedTools(roots: string[]) {
|
|
|
78
92
|
);
|
|
79
93
|
}
|
|
80
94
|
|
|
95
|
+
export type ChildFailureKind = "failed" | "cancelled" | "timed-out";
|
|
96
|
+
|
|
81
97
|
export async function runChildSession(input: {
|
|
82
98
|
cwd: string;
|
|
83
99
|
modelRuntime: ModelRuntime;
|
|
@@ -89,6 +105,7 @@ export async function runChildSession(input: {
|
|
|
89
105
|
output: "evidence" | "diff";
|
|
90
106
|
roots: string[];
|
|
91
107
|
timeoutMs: number;
|
|
108
|
+
signal?: AbortSignal;
|
|
92
109
|
}): Promise<
|
|
93
110
|
| {
|
|
94
111
|
ok: true;
|
|
@@ -102,6 +119,7 @@ export async function runChildSession(input: {
|
|
|
102
119
|
| {
|
|
103
120
|
ok: false;
|
|
104
121
|
error: string;
|
|
122
|
+
failureKind: ChildFailureKind;
|
|
105
123
|
disposeCount: number;
|
|
106
124
|
classification: SubmitClassification;
|
|
107
125
|
}
|
|
@@ -118,10 +136,18 @@ export async function runChildSession(input: {
|
|
|
118
136
|
let disposeCount = 0;
|
|
119
137
|
const usage = new UsageAggregator();
|
|
120
138
|
const abort = new AbortController();
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
139
|
+
const forwardCancellation = () =>
|
|
140
|
+
abort.abort(input.signal?.reason ?? new Error("child phase cancelled"));
|
|
141
|
+
if (input.signal?.aborted) forwardCancellation();
|
|
142
|
+
else
|
|
143
|
+
input.signal?.addEventListener("abort", forwardCancellation, {
|
|
144
|
+
once: true,
|
|
145
|
+
});
|
|
146
|
+
let timedOut = false;
|
|
147
|
+
const timer = setTimeout(() => {
|
|
148
|
+
timedOut = true;
|
|
149
|
+
abort.abort(new Error("child phase timeout"));
|
|
150
|
+
}, input.timeoutMs);
|
|
125
151
|
let session:
|
|
126
152
|
| Awaited<ReturnType<typeof createAgentSession>>["session"]
|
|
127
153
|
| undefined;
|
|
@@ -160,7 +186,8 @@ export async function runChildSession(input: {
|
|
|
160
186
|
};
|
|
161
187
|
};
|
|
162
188
|
try {
|
|
163
|
-
(
|
|
189
|
+
abort.signal.throwIfAborted();
|
|
190
|
+
const creation = createAgentSession({
|
|
164
191
|
cwd: input.cwd,
|
|
165
192
|
modelRuntime: input.modelRuntime,
|
|
166
193
|
model: input.model,
|
|
@@ -173,7 +200,16 @@ export async function runChildSession(input: {
|
|
|
173
200
|
compaction: { enabled: false },
|
|
174
201
|
retry: { enabled: false, maxRetries: 0, provider: { maxRetries: 0 } },
|
|
175
202
|
}),
|
|
176
|
-
})
|
|
203
|
+
});
|
|
204
|
+
try {
|
|
205
|
+
({ session } = await abortable(creation, abort.signal));
|
|
206
|
+
} catch (error) {
|
|
207
|
+
void creation
|
|
208
|
+
.then(({ session: lateSession }) => lateSession.dispose())
|
|
209
|
+
.catch(() => undefined);
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
abort.signal.throwIfAborted();
|
|
177
213
|
unsubscribe = session.subscribe((event) => {
|
|
178
214
|
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
179
215
|
usage.add(
|
|
@@ -185,6 +221,7 @@ export async function runChildSession(input: {
|
|
|
185
221
|
const onAbort = () => void session?.abort();
|
|
186
222
|
abort.signal.addEventListener("abort", onAbort, { once: true });
|
|
187
223
|
try {
|
|
224
|
+
if (abort.signal.aborted) throw abort.signal.reason;
|
|
188
225
|
await Promise.race([
|
|
189
226
|
session.prompt(input.systemPrompt, { expandPromptTemplates: false }),
|
|
190
227
|
new Promise<never>((_, reject) =>
|
|
@@ -206,6 +243,7 @@ export async function runChildSession(input: {
|
|
|
206
243
|
return {
|
|
207
244
|
ok: false,
|
|
208
245
|
error: "child did not retain exactly one structural submission",
|
|
246
|
+
failureKind: "failed",
|
|
209
247
|
disposeCount,
|
|
210
248
|
classification,
|
|
211
249
|
};
|
|
@@ -223,6 +261,7 @@ export async function runChildSession(input: {
|
|
|
223
261
|
return {
|
|
224
262
|
ok: false,
|
|
225
263
|
error: "final assistant message is not one structural submit",
|
|
264
|
+
failureKind: "failed",
|
|
226
265
|
disposeCount,
|
|
227
266
|
classification,
|
|
228
267
|
};
|
|
@@ -239,14 +278,22 @@ export async function runChildSession(input: {
|
|
|
239
278
|
};
|
|
240
279
|
} catch (error) {
|
|
241
280
|
disposeOnce();
|
|
281
|
+
const failureKind: ChildFailureKind = timedOut
|
|
282
|
+
? "timed-out"
|
|
283
|
+
: input.signal !== undefined &&
|
|
284
|
+
(input.signal.aborted || abort.signal.reason === input.signal.reason)
|
|
285
|
+
? "cancelled"
|
|
286
|
+
: "failed";
|
|
242
287
|
return {
|
|
243
288
|
ok: false,
|
|
244
289
|
error: (error as Error).message,
|
|
290
|
+
failureKind,
|
|
245
291
|
disposeCount,
|
|
246
292
|
classification: classifySession(),
|
|
247
293
|
};
|
|
248
294
|
} finally {
|
|
249
295
|
clearTimeout(timer);
|
|
296
|
+
input.signal?.removeEventListener("abort", forwardCancellation);
|
|
250
297
|
unsubscribe?.();
|
|
251
298
|
disposeOnce();
|
|
252
299
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,15 +3,97 @@
|
|
|
3
3
|
// the active set at session start. Eligible-stage activation is wired by the
|
|
4
4
|
// workflow routing (abel-design/implement/diagnose provenance) in the prompts
|
|
5
5
|
// integration; abel-init and ordinary prompts never activate dispatch.
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import type {
|
|
9
|
+
AgentToolResult,
|
|
10
|
+
AgentToolUpdateCallback,
|
|
11
|
+
ExtensionAPI,
|
|
12
|
+
ExtensionContext,
|
|
13
|
+
Theme,
|
|
14
|
+
ToolRenderResultOptions,
|
|
15
|
+
} from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { type Activation, activateTool, deactivateTool } from "./activation.ts";
|
|
8
17
|
import { ACTIONS } from "./contracts.ts";
|
|
9
18
|
import { Runtime } from "./runtime.ts";
|
|
19
|
+
import {
|
|
20
|
+
ACTIVITY_DETAILS_KEY,
|
|
21
|
+
ActivityController,
|
|
22
|
+
renderActivityCall,
|
|
23
|
+
renderActivityResult,
|
|
24
|
+
} from "./subagent-activity.ts";
|
|
10
25
|
|
|
11
26
|
export const DISPATCH_TOOL = "abel_dispatch";
|
|
12
27
|
|
|
28
|
+
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
29
|
+
const ELIGIBLE_PROMPTS = [
|
|
30
|
+
"abel-design",
|
|
31
|
+
"abel-implement",
|
|
32
|
+
"abel-diagnose",
|
|
33
|
+
] as const;
|
|
34
|
+
type EligiblePrompt = (typeof ELIGIBLE_PROMPTS)[number];
|
|
35
|
+
|
|
36
|
+
function invokedPrompt(text: string): EligiblePrompt | undefined {
|
|
37
|
+
const name = text.match(/^\/([^\s]+)(?:\s|$)/)?.[1];
|
|
38
|
+
return ELIGIBLE_PROMPTS.find((candidate) => candidate === name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function promptMarker(name: EligiblePrompt): string {
|
|
42
|
+
return `<!-- ABEL:PROMPT:${name} -->`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hasPackageProvenance(pi: ExtensionAPI, name: EligiblePrompt): boolean {
|
|
46
|
+
const commands = pi
|
|
47
|
+
.getCommands()
|
|
48
|
+
.filter((candidate) => candidate.name === name);
|
|
49
|
+
return (
|
|
50
|
+
commands.length === 1 &&
|
|
51
|
+
commands[0]?.source === "prompt" &&
|
|
52
|
+
commands[0].sourceInfo.origin === "package" &&
|
|
53
|
+
commands[0].sourceInfo.baseDir === PACKAGE_ROOT &&
|
|
54
|
+
commands[0].sourceInfo.path === join(PACKAGE_ROOT, "prompts", `${name}.md`)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function hasExpandedPromptMarker(
|
|
59
|
+
prompt: string,
|
|
60
|
+
name: EligiblePrompt,
|
|
61
|
+
): boolean {
|
|
62
|
+
const requestEnd = prompt.lastIndexOf("</abel-request>");
|
|
63
|
+
if (requestEnd < 0) return false;
|
|
64
|
+
const body = prompt.slice(requestEnd + "</abel-request>".length);
|
|
65
|
+
const marker = promptMarker(name);
|
|
66
|
+
return (
|
|
67
|
+
body.includes(marker) && body.indexOf(marker) === body.lastIndexOf(marker)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function activateDispatcher(
|
|
72
|
+
pi: ExtensionAPI,
|
|
73
|
+
activation: Activation,
|
|
74
|
+
name: EligiblePrompt,
|
|
75
|
+
prompt: string,
|
|
76
|
+
): void {
|
|
77
|
+
if (
|
|
78
|
+
!hasPackageProvenance(pi, name) ||
|
|
79
|
+
!hasExpandedPromptMarker(prompt, name)
|
|
80
|
+
) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!activation.isActive()) {
|
|
84
|
+
if (activation.state !== "inactive") return;
|
|
85
|
+
activation.request();
|
|
86
|
+
activation.activate();
|
|
87
|
+
}
|
|
88
|
+
const active = pi.getActiveTools();
|
|
89
|
+
if (!active.includes(DISPATCH_TOOL)) {
|
|
90
|
+
pi.setActiveTools(activateTool(active, DISPATCH_TOOL));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
13
94
|
export default function register(pi: ExtensionAPI): void {
|
|
14
95
|
const runtime = new Runtime();
|
|
96
|
+
const activity = new ActivityController();
|
|
15
97
|
|
|
16
98
|
pi.registerTool({
|
|
17
99
|
name: DISPATCH_TOOL,
|
|
@@ -34,30 +116,78 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
34
116
|
required: ["action"],
|
|
35
117
|
},
|
|
36
118
|
async execute(
|
|
37
|
-
|
|
119
|
+
toolCallId: string,
|
|
38
120
|
params: { action?: string; request?: unknown; resultId?: string },
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
ctx:
|
|
121
|
+
signal: AbortSignal | undefined,
|
|
122
|
+
onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
|
123
|
+
ctx: ExtensionContext,
|
|
42
124
|
) {
|
|
43
125
|
const action = typeof params?.action === "string" ? params.action : "";
|
|
44
|
-
const
|
|
126
|
+
const validRun =
|
|
127
|
+
action === "run" && runtime.validateRequest(params.request).ok;
|
|
128
|
+
const tuiRun = ctx.mode === "tui" && validRun;
|
|
129
|
+
const result = tuiRun
|
|
130
|
+
? await runtime.execute(
|
|
131
|
+
action,
|
|
132
|
+
params,
|
|
133
|
+
ctx,
|
|
134
|
+
signal,
|
|
135
|
+
activity.observe(
|
|
136
|
+
toolCallId,
|
|
137
|
+
onUpdate as ((result: unknown) => void) | undefined,
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
: await runtime.execute(action, params, ctx, signal);
|
|
141
|
+
const display = tuiRun
|
|
142
|
+
? activity.finalize(toolCallId, result)
|
|
143
|
+
: undefined;
|
|
144
|
+
const details = display
|
|
145
|
+
? { ...result, [ACTIVITY_DETAILS_KEY]: display }
|
|
146
|
+
: result;
|
|
45
147
|
if (result.ok) {
|
|
46
148
|
return {
|
|
47
149
|
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
48
|
-
details
|
|
150
|
+
details,
|
|
49
151
|
usage: result.usage,
|
|
50
152
|
};
|
|
51
153
|
}
|
|
52
154
|
return {
|
|
53
155
|
content: [{ type: "text", text: result.error }],
|
|
54
|
-
details
|
|
156
|
+
details,
|
|
55
157
|
isError: true,
|
|
56
158
|
};
|
|
57
159
|
},
|
|
160
|
+
renderCall(args: unknown, theme: Theme, _context: unknown) {
|
|
161
|
+
return renderActivityCall(args, theme);
|
|
162
|
+
},
|
|
163
|
+
renderResult(
|
|
164
|
+
result: AgentToolResult<unknown>,
|
|
165
|
+
options: ToolRenderResultOptions,
|
|
166
|
+
theme: Theme,
|
|
167
|
+
_context: unknown,
|
|
168
|
+
) {
|
|
169
|
+
return renderActivityResult(result, options, theme);
|
|
170
|
+
},
|
|
58
171
|
} as never);
|
|
59
172
|
|
|
60
|
-
|
|
173
|
+
let pendingPrompt: EligiblePrompt | undefined;
|
|
174
|
+
|
|
175
|
+
pi.on("input", (event) => {
|
|
176
|
+
pendingPrompt = invokedPrompt(event.text);
|
|
177
|
+
return { action: "continue" };
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
pi.on("before_agent_start", (event) => {
|
|
181
|
+
const prompt = pendingPrompt;
|
|
182
|
+
pendingPrompt = undefined;
|
|
183
|
+
if (prompt)
|
|
184
|
+
activateDispatcher(pi, runtime.activation, prompt, event.prompt);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
pi.on("session_start", (_event, ctx) => {
|
|
188
|
+
pendingPrompt = undefined;
|
|
189
|
+
activity.detach();
|
|
190
|
+
if (ctx.mode === "tui") activity.attach(ctx.ui);
|
|
61
191
|
// Restore the default: dispatch is inactive unless a verified stage
|
|
62
192
|
// reactivates it through the activation helpers.
|
|
63
193
|
const active = pi.getActiveTools();
|
|
@@ -66,8 +196,10 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
66
196
|
}
|
|
67
197
|
});
|
|
68
198
|
|
|
69
|
-
pi.on("session_shutdown", () => {
|
|
70
|
-
|
|
199
|
+
pi.on("session_shutdown", async () => {
|
|
200
|
+
activity.detach();
|
|
201
|
+
await runtime.drain();
|
|
202
|
+
activity.clear();
|
|
71
203
|
const active = pi.getActiveTools();
|
|
72
204
|
if (active.includes(DISPATCH_TOOL)) {
|
|
73
205
|
pi.setActiveTools(deactivateTool(active, DISPATCH_TOOL));
|
package/src/parent-provider.ts
CHANGED
|
@@ -10,12 +10,19 @@ import {
|
|
|
10
10
|
|
|
11
11
|
export async function runtimeForProvider(
|
|
12
12
|
provider: Provider,
|
|
13
|
+
signal?: AbortSignal,
|
|
13
14
|
): Promise<ModelRuntime> {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
signal?.throwIfAborted();
|
|
16
|
+
const runtime = await abortable(
|
|
17
|
+
ModelRuntime.create({
|
|
18
|
+
credentials: new InMemoryCredentialStore(),
|
|
19
|
+
modelsPath: null,
|
|
20
|
+
refreshOnCreate: false,
|
|
21
|
+
signal,
|
|
22
|
+
}),
|
|
23
|
+
signal,
|
|
24
|
+
);
|
|
25
|
+
signal?.throwIfAborted();
|
|
19
26
|
runtime.registerNativeProvider(provider);
|
|
20
27
|
return runtime;
|
|
21
28
|
}
|
|
@@ -69,17 +76,39 @@ export function phaseProvider(
|
|
|
69
76
|
|
|
70
77
|
export async function runtimeFromContext(
|
|
71
78
|
ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
|
|
79
|
+
signal?: AbortSignal,
|
|
72
80
|
): Promise<{ modelRuntime: ModelRuntime; model: Model<string> }> {
|
|
81
|
+
signal?.throwIfAborted();
|
|
73
82
|
if (!ctx.model) throw new Error("parent model is unavailable");
|
|
74
83
|
const parent = ctx.modelRegistry.getProvider(ctx.model.provider);
|
|
75
84
|
if (!parent) throw new Error("parent Provider is unavailable");
|
|
76
|
-
const resolved = await
|
|
85
|
+
const resolved = await abortable(
|
|
86
|
+
ctx.modelRegistry.getApiKeyAndHeaders(ctx.model),
|
|
87
|
+
signal,
|
|
88
|
+
);
|
|
77
89
|
if (!resolved.ok) throw new Error(resolved.error);
|
|
90
|
+
signal?.throwIfAborted();
|
|
78
91
|
const provider = phaseProvider(parent, resolved);
|
|
79
|
-
const modelRuntime = await runtimeForProvider(provider);
|
|
92
|
+
const modelRuntime = await runtimeForProvider(provider, signal);
|
|
93
|
+
signal?.throwIfAborted();
|
|
80
94
|
const model = {
|
|
81
95
|
...ctx.model,
|
|
82
96
|
baseUrl: resolved.baseUrl ?? ctx.model.baseUrl,
|
|
83
97
|
} as Model<string>;
|
|
84
98
|
return { modelRuntime, model };
|
|
85
99
|
}
|
|
100
|
+
|
|
101
|
+
function abortable<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
102
|
+
if (!signal) return promise;
|
|
103
|
+
signal.throwIfAborted();
|
|
104
|
+
return new Promise<T>((resolve, reject) => {
|
|
105
|
+
const onAbort = () => {
|
|
106
|
+
signal.removeEventListener("abort", onAbort);
|
|
107
|
+
reject(signal.reason);
|
|
108
|
+
};
|
|
109
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
110
|
+
promise
|
|
111
|
+
.then(resolve, reject)
|
|
112
|
+
.finally(() => signal.removeEventListener("abort", onAbort));
|
|
113
|
+
});
|
|
114
|
+
}
|