pi-t3-bridge 0.5.0 → 0.7.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/README.md +20 -0
- package/amplike.ts +46 -0
- package/package.json +7 -6
- package/src/amplike/btw.ts +89 -0
- package/src/amplike/handoff.ts +200 -0
- package/src/amplike/package.ts +136 -0
- package/src/amplike/selection.ts +144 -0
- package/src/amplike/t3-view.ts +88 -0
- package/src/bridge/accounts.ts +66 -49
- package/src/bridge/ask.ts +73 -93
- package/src/bridge/bash-timeout.ts +15 -8
- package/src/bridge/config.ts +29 -26
- package/src/bridge/plan.ts +19 -11
- package/src/bridge/resume.ts +17 -11
- package/src/bridge/state.ts +54 -34
- package/src/bridge/steer-judge.ts +130 -96
- package/src/bridge/steer-transcript.ts +4 -3
- package/src/bridge/steer.ts +16 -10
- package/src/bridge/t3-json.ts +14 -19
- package/src/bridge/tasks.ts +39 -31
- package/src/bridge/thread.ts +5 -6
- package/src/bridge/transcript.ts +11 -8
- package/src/pstack/brief.ts +4 -3
- package/src/pstack/child.ts +45 -32
- package/src/pstack/models.ts +138 -106
- package/src/pstack/overlay.ts +18 -20
- package/src/pstack/results.ts +4 -7
- package/src/pstack/sandbox.ts +12 -8
- package/pstack/skills/statechart/scripts/node_modules/.package-lock.json +0 -46
- package/pstack/skills/statechart/scripts/node_modules/fast-check/LICENSE +0 -21
- package/pstack/skills/statechart/scripts/node_modules/fast-check/README.md +0 -263
- package/pstack/skills/statechart/scripts/node_modules/fast-check/lib/cjs/fast-check.d.ts +0 -5165
- package/pstack/skills/statechart/scripts/node_modules/fast-check/lib/cjs/fast-check.js +0 -12147
- package/pstack/skills/statechart/scripts/node_modules/fast-check/lib/cjs/package.json +0 -3
- package/pstack/skills/statechart/scripts/node_modules/fast-check/lib/cjs/types57/fast-check.d.ts +0 -5165
- package/pstack/skills/statechart/scripts/node_modules/fast-check/lib/fast-check.d.ts +0 -5165
- package/pstack/skills/statechart/scripts/node_modules/fast-check/lib/fast-check.js +0 -12011
- package/pstack/skills/statechart/scripts/node_modules/fast-check/lib/rolldown-runtime-D7D4PA-g.js +0 -13
- package/pstack/skills/statechart/scripts/node_modules/fast-check/lib/types57/fast-check.d.ts +0 -5165
- package/pstack/skills/statechart/scripts/node_modules/fast-check/package.json +0 -94
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/LICENSE +0 -21
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/README.md +0 -168
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/RandomGenerator-DcXj09Ch.d.ts +0 -14
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/distribution/uniformBigInt.d.ts +0 -15
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/distribution/uniformBigInt.js +0 -38
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/distribution/uniformFloat32.d.ts +0 -15
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/distribution/uniformFloat32.js +0 -18
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/distribution/uniformFloat64.d.ts +0 -15
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/distribution/uniformFloat64.js +0 -22
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/distribution/uniformInt.d.ts +0 -15
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/distribution/uniformInt.js +0 -134
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/RandomGenerator-DcXj09Ch.d.ts +0 -14
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/distribution/uniformBigInt.d.ts +0 -15
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/distribution/uniformBigInt.js +0 -37
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/distribution/uniformFloat32.d.ts +0 -15
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/distribution/uniformFloat32.js +0 -17
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/distribution/uniformFloat64.d.ts +0 -15
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/distribution/uniformFloat64.js +0 -21
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/distribution/uniformInt.d.ts +0 -15
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/distribution/uniformInt.js +0 -133
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/generator/congruential32.d.ts +0 -7
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/generator/congruential32.js +0 -44
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/generator/mersenne.d.ts +0 -7
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/generator/mersenne.js +0 -90
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/generator/xoroshiro128plus.d.ts +0 -7
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/generator/xoroshiro128plus.js +0 -80
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/generator/xorshift128plus.d.ts +0 -7
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/generator/xorshift128plus.js +0 -78
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/package.json +0 -3
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/types/JumpableRandomGenerator.d.ts +0 -16
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/types/JumpableRandomGenerator.js +0 -0
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/types/RandomGenerator.d.ts +0 -2
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/types/RandomGenerator.js +0 -0
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/utils/generateN.d.ts +0 -6
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/utils/generateN.js +0 -8
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/utils/purify.d.ts +0 -12
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/utils/purify.js +0 -9
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/utils/skipN.d.ts +0 -6
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/utils/skipN.js +0 -6
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/generator/congruential32.d.ts +0 -7
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/generator/congruential32.js +0 -46
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/generator/mersenne.d.ts +0 -7
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/generator/mersenne.js +0 -92
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/generator/xoroshiro128plus.d.ts +0 -7
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/generator/xoroshiro128plus.js +0 -82
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/generator/xorshift128plus.d.ts +0 -7
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/generator/xorshift128plus.js +0 -80
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/types/JumpableRandomGenerator.d.ts +0 -16
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/types/JumpableRandomGenerator.js +0 -0
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/types/RandomGenerator.d.ts +0 -2
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/types/RandomGenerator.js +0 -0
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/utils/generateN.d.ts +0 -6
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/utils/generateN.js +0 -9
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/utils/purify.d.ts +0 -12
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/utils/purify.js +0 -10
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/utils/skipN.d.ts +0 -6
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/utils/skipN.js +0 -7
- package/pstack/skills/statechart/scripts/node_modules/pure-rand/package.json +0 -133
package/README.md
CHANGED
|
@@ -223,6 +223,26 @@ sequenceDiagram
|
|
|
223
223
|
Note over Parent: the extension adds the report from the ledger
|
|
224
224
|
```
|
|
225
225
|
|
|
226
|
+
## pi-amplike in T3
|
|
227
|
+
|
|
228
|
+
`amplike.ts` adapts [pi-amplike](https://github.com/pasky/pi-amplike) for T3 threads. T3 shows Pi's `notify`, `select`, `confirm`, `input` and `editor` calls. It drops widgets, status lines, custom UI and key presses, and it resumes a thread from the session file it first recorded. So in T3, amplike's subagents, `/btw` and handoff show nothing or break. To use the adapter, turn off amplike's own extensions in `~/.pi/agent/settings.json` and keep its skills:
|
|
229
|
+
|
|
230
|
+
```json
|
|
231
|
+
{ "source": "npm:pi-amplike", "extensions": [] }
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`amplike.ts` then loads amplike's extensions itself. Outside T3 they load unchanged. In a T3 thread, the adapter changes these parts:
|
|
235
|
+
|
|
236
|
+
| amplike | In a T3 thread |
|
|
237
|
+
| --- | --- |
|
|
238
|
+
| `subagent` tool | Off. T3's `delegate_task` runs subagents. |
|
|
239
|
+
| `/btw <prompt>` | Starts a `delegate_task` child that reads this thread's session with `session_query`. Its answer wakes this thread. |
|
|
240
|
+
| `handoff` tool and `/handoff <goal>` | Writes a summary with the session model, then starts a new T3 thread with `t3_thread_launch`. The new thread gets the goal, the summary and the parent session file. This thread keeps its session. |
|
|
241
|
+
| `/mode` | Works as in the terminal. Each model or thinking change also goes to `t3_thread_configure`, so T3's model picker matches. |
|
|
242
|
+
| Shortcuts | Off. T3 sends Pi no key presses. |
|
|
243
|
+
|
|
244
|
+
`session_query`, `/permissions` and amplike's skills work unchanged. While amplike still loads its own extensions, the adapter does nothing, and a T3 thread shows a notice with the settings change.
|
|
245
|
+
|
|
226
246
|
## Configuration
|
|
227
247
|
|
|
228
248
|
The bridge reads one JSON file per machine, at `~/.config/pi-t3/config.json`. Every key is optional. The bridge reads the file again on each prompt, so an edit applies on the next prompt. When one value is invalid, only the invalid key falls back to its default, and the bridge tells you once.
|
package/amplike.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
findAmplike,
|
|
5
|
+
loadFactory,
|
|
6
|
+
loadModeSpecFrom,
|
|
7
|
+
} from "./src/amplike/package.ts";
|
|
8
|
+
import { t3View } from "./src/amplike/t3-view.ts";
|
|
9
|
+
import { insideT3 } from "./src/bridge/t3-http.ts";
|
|
10
|
+
|
|
11
|
+
// Loads pi-amplike's extensions in place of Pi, so a T3 thread gets T3's
|
|
12
|
+
// versions of the parts T3 can't show. Outside T3 amplike loads unchanged.
|
|
13
|
+
// It runs only when the settings entry for amplike turns its extensions off.
|
|
14
|
+
const amplikeExtension = async (pi: ExtensionAPI): Promise<void> => {
|
|
15
|
+
const amplike = await findAmplike(process.cwd());
|
|
16
|
+
|
|
17
|
+
if (amplike === undefined) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (amplike.selfLoaded) {
|
|
22
|
+
if (insideT3()) {
|
|
23
|
+
pi.on("session_start", (_event, ctx) => {
|
|
24
|
+
if (ctx.hasUI) {
|
|
25
|
+
ctx.ui.notify(
|
|
26
|
+
`pi-t3 can't adapt pi-amplike for T3 while pi-amplike loads its own extensions. In ~/.pi/agent/settings.json, change "${amplike.source}" to {"source":"${amplike.source}","extensions":[]}.`,
|
|
27
|
+
"warning"
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const host = insideT3()
|
|
37
|
+
? t3View(pi, await loadModeSpecFrom(amplike.root))
|
|
38
|
+
: pi;
|
|
39
|
+
|
|
40
|
+
const factories = await Promise.all(amplike.extensions.map(loadFactory));
|
|
41
|
+
|
|
42
|
+
// Each call starts in file order, as when Pi loads the files itself.
|
|
43
|
+
await Promise.all(factories.map((factory) => factory(host)));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default amplikeExtension;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-t3-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Pi extension adapter for t3-pstack inside T3 Code.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-extension",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"overlay",
|
|
39
39
|
"LICENSE",
|
|
40
40
|
"tstack.ts",
|
|
41
|
-
"tstack"
|
|
41
|
+
"tstack",
|
|
42
|
+
"amplike.ts"
|
|
42
43
|
],
|
|
43
44
|
"type": "module",
|
|
44
45
|
"scripts": {
|
|
@@ -49,6 +50,7 @@
|
|
|
49
50
|
"typecheck": "tsc --project tsconfig.json && tsc --project pstack/skills/poteto-mode/scripts/watch-pr/tsconfig.json && tsc --project pstack/skills/statechart/scripts/tsconfig.json"
|
|
50
51
|
},
|
|
51
52
|
"dependencies": {
|
|
53
|
+
"@earendil-works/pi-schema": "1.5.0",
|
|
52
54
|
"commander": "^15.0.0",
|
|
53
55
|
"marked": "18.0.11",
|
|
54
56
|
"pele": "1.13.0"
|
|
@@ -60,14 +62,12 @@
|
|
|
60
62
|
"oxfmt": "^0.74.0",
|
|
61
63
|
"oxlint": "^1.89.0",
|
|
62
64
|
"testcontainers": "^12.2.1",
|
|
63
|
-
"typebox": "1.3.27",
|
|
64
65
|
"typescript": "^7.0.2",
|
|
65
66
|
"ultracite": "^8.0.1"
|
|
66
67
|
},
|
|
67
68
|
"peerDependencies": {
|
|
68
69
|
"@earendil-works/pi-ai": "*",
|
|
69
|
-
"@earendil-works/pi-coding-agent": "*"
|
|
70
|
-
"typebox": "*"
|
|
70
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
71
71
|
},
|
|
72
72
|
"peerDependenciesMeta": {
|
|
73
73
|
"@earendil-works/pi-coding-agent": {
|
|
@@ -81,7 +81,8 @@
|
|
|
81
81
|
"extensions": [
|
|
82
82
|
"./bridge.ts",
|
|
83
83
|
"./tstack.ts",
|
|
84
|
-
"./pstack.ts"
|
|
84
|
+
"./pstack.ts",
|
|
85
|
+
"./amplike.ts"
|
|
85
86
|
]
|
|
86
87
|
},
|
|
87
88
|
"upstreams": {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { compile, nullable, obj, opt, str } from "@earendil-works/pi-schema";
|
|
3
|
+
|
|
4
|
+
import { callT3Tool } from "../bridge/t3-http.ts";
|
|
5
|
+
import type { LoadModeSpec } from "./package.ts";
|
|
6
|
+
import {
|
|
7
|
+
changeFromFlags,
|
|
8
|
+
currentSelection,
|
|
9
|
+
isEmptyChange,
|
|
10
|
+
parseFlags,
|
|
11
|
+
} from "./selection.ts";
|
|
12
|
+
import type { SelectionChange } from "./selection.ts";
|
|
13
|
+
|
|
14
|
+
type Command = Parameters<ExtensionAPI["registerCommand"]>[1];
|
|
15
|
+
|
|
16
|
+
const isDelegated = compile(
|
|
17
|
+
obj({ link: opt(nullable(str())), taskId: str() }, { open: true })
|
|
18
|
+
).check;
|
|
19
|
+
|
|
20
|
+
const TITLE_LENGTH = 80;
|
|
21
|
+
|
|
22
|
+
const USAGE = "Usage: /btw [-mode <name>] [-model <provider/id>] <prompt>";
|
|
23
|
+
|
|
24
|
+
// delegate_task's target for a Pi child: model as "provider/modelId", and the
|
|
25
|
+
// thinking level as an option. A flag that names only one keeps the other.
|
|
26
|
+
const targetFor = async (change: SelectionChange) => {
|
|
27
|
+
const current = await currentSelection();
|
|
28
|
+
const options: Record<string, string> = {};
|
|
29
|
+
|
|
30
|
+
if (change.thinking !== undefined) {
|
|
31
|
+
options.thinking = change.thinking;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { model: change.model ?? current.model, options };
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// In T3, /btw is a delegate_task child. The child shows up in T3, and its
|
|
38
|
+
// result wakes this thread when it finishes.
|
|
39
|
+
export const btwCommand = (loadModeSpec: LoadModeSpec): Command => ({
|
|
40
|
+
description:
|
|
41
|
+
"Ask a side question in a T3 child thread: /btw [-mode <name>] [-model <provider/id>] <prompt>",
|
|
42
|
+
handler: async (args, ctx) => {
|
|
43
|
+
try {
|
|
44
|
+
const { flags, text } = parseFlags(args);
|
|
45
|
+
|
|
46
|
+
if (text.length === 0) {
|
|
47
|
+
throw new Error(USAGE);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const change = await changeFromFlags(flags, ctx.cwd, loadModeSpec);
|
|
51
|
+
const parent = ctx.sessionManager.getSessionFile();
|
|
52
|
+
|
|
53
|
+
const task = [
|
|
54
|
+
"The user asked this side question while working in another thread. Answer it. Inspect only, change no file.",
|
|
55
|
+
text,
|
|
56
|
+
...(parent === undefined
|
|
57
|
+
? []
|
|
58
|
+
: [
|
|
59
|
+
`The other thread's Pi session file is \`${parent}\`. Call session_query on it for any context the question needs.`,
|
|
60
|
+
]),
|
|
61
|
+
].join("\n\n");
|
|
62
|
+
|
|
63
|
+
const delegate = {
|
|
64
|
+
mode: "async",
|
|
65
|
+
role: "research",
|
|
66
|
+
task,
|
|
67
|
+
title: `btw: ${text}`.slice(0, TITLE_LENGTH),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const delegated = isEmptyChange(change)
|
|
71
|
+
? await callT3Tool("delegate_task", delegate)
|
|
72
|
+
: await callT3Tool("delegate_task", {
|
|
73
|
+
...delegate,
|
|
74
|
+
target: await targetFor(change),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!isDelegated(delegated)) {
|
|
78
|
+
throw new Error("delegate_task returned no task.");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
ctx.ui.notify(
|
|
82
|
+
`btw started: ${delegated.link ?? delegated.taskId}`,
|
|
83
|
+
"info"
|
|
84
|
+
);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
ctx.ui.notify(`btw failed: ${String(error)}`, "error");
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
2
|
+
import {
|
|
3
|
+
convertToLlm,
|
|
4
|
+
defineTool,
|
|
5
|
+
serializeConversation,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type {
|
|
8
|
+
ExtensionAPI,
|
|
9
|
+
ExtensionContext,
|
|
10
|
+
} from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { compile, nullable, obj, opt, str } from "@earendil-works/pi-schema";
|
|
12
|
+
import type { Static } from "@earendil-works/pi-schema";
|
|
13
|
+
|
|
14
|
+
import { callT3Tool } from "../bridge/t3-http.ts";
|
|
15
|
+
import type { LoadModeSpec } from "./package.ts";
|
|
16
|
+
import {
|
|
17
|
+
applyChange,
|
|
18
|
+
changeFromFlags,
|
|
19
|
+
currentSelection,
|
|
20
|
+
isEmptyChange,
|
|
21
|
+
parseFlags,
|
|
22
|
+
} from "./selection.ts";
|
|
23
|
+
import type { Flags } from "./selection.ts";
|
|
24
|
+
|
|
25
|
+
type Command = Parameters<ExtensionAPI["registerCommand"]>[1];
|
|
26
|
+
|
|
27
|
+
const SUMMARY_PROMPT = `You write the first message of a new thread that continues work from a conversation. Given the conversation and the user's goal for the new thread, write a self-contained prompt with:
|
|
28
|
+
|
|
29
|
+
## Context
|
|
30
|
+
The decisions made, approaches taken, and key findings that matter for the goal.
|
|
31
|
+
|
|
32
|
+
Files involved:
|
|
33
|
+
- each relevant file that was discussed or changed
|
|
34
|
+
|
|
35
|
+
## Task
|
|
36
|
+
What the new thread should do next, based on the goal.
|
|
37
|
+
|
|
38
|
+
Output only the prompt, with no preamble.`;
|
|
39
|
+
|
|
40
|
+
const TITLE_LENGTH = 80;
|
|
41
|
+
|
|
42
|
+
const isLaunched = compile(
|
|
43
|
+
obj({ link: opt(nullable(str())), threadId: str() }, { open: true })
|
|
44
|
+
).check;
|
|
45
|
+
|
|
46
|
+
const HandoffSchema = obj(
|
|
47
|
+
{
|
|
48
|
+
goal: str({ description: "The goal/task for the new thread" }),
|
|
49
|
+
mode: opt(
|
|
50
|
+
str({
|
|
51
|
+
description:
|
|
52
|
+
"Amplike mode name to start the new thread with (e.g. 'rush', 'smart', 'deep')",
|
|
53
|
+
})
|
|
54
|
+
),
|
|
55
|
+
model: opt(
|
|
56
|
+
str({
|
|
57
|
+
description:
|
|
58
|
+
"Model to start the new thread with, as provider/modelId (e.g. 'anthropic/claude-haiku-4-5')",
|
|
59
|
+
})
|
|
60
|
+
),
|
|
61
|
+
},
|
|
62
|
+
{ open: true }
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const summarize = async (
|
|
66
|
+
ctx: ExtensionContext,
|
|
67
|
+
goal: string,
|
|
68
|
+
signal: AbortSignal | undefined
|
|
69
|
+
): Promise<string> => {
|
|
70
|
+
const { model } = ctx;
|
|
71
|
+
|
|
72
|
+
if (model === undefined) {
|
|
73
|
+
throw new Error("No model selected.");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const messages = ctx.sessionManager
|
|
77
|
+
.getBranch()
|
|
78
|
+
.flatMap((entry) => (entry.type === "message" ? [entry.message] : []));
|
|
79
|
+
|
|
80
|
+
if (messages.length === 0) {
|
|
81
|
+
throw new Error("No conversation to hand off.");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const request: Message = {
|
|
85
|
+
content: [
|
|
86
|
+
{
|
|
87
|
+
text: `## Conversation\n\n${serializeConversation(convertToLlm(messages))}\n\n## Goal for the new thread\n\n${goal}`,
|
|
88
|
+
type: "text",
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
role: "user",
|
|
92
|
+
timestamp: Date.now(),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const response = await ctx.modelRegistry
|
|
96
|
+
.streamSimple(
|
|
97
|
+
model,
|
|
98
|
+
{ messages: [request], systemPrompt: SUMMARY_PROMPT },
|
|
99
|
+
{ signal }
|
|
100
|
+
)
|
|
101
|
+
.result();
|
|
102
|
+
|
|
103
|
+
if (response.stopReason === "aborted" || response.stopReason === "error") {
|
|
104
|
+
throw new Error(
|
|
105
|
+
response.errorMessage ?? `The summary ${response.stopReason}.`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return response.content
|
|
110
|
+
.flatMap((part) => (part.type === "text" ? [part.text] : []))
|
|
111
|
+
.join("\n");
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// In T3 a handoff is a new T3 thread. Pi keeps its own session file, which is
|
|
115
|
+
// the one T3 resumes this thread from.
|
|
116
|
+
const handoff = async (
|
|
117
|
+
ctx: ExtensionContext,
|
|
118
|
+
goal: string,
|
|
119
|
+
flags: Flags,
|
|
120
|
+
loadModeSpec: LoadModeSpec,
|
|
121
|
+
signal: AbortSignal | undefined
|
|
122
|
+
): Promise<string> => {
|
|
123
|
+
const change = await changeFromFlags(flags, ctx.cwd, loadModeSpec);
|
|
124
|
+
const summary = await summarize(ctx, goal, signal);
|
|
125
|
+
const parent = ctx.sessionManager.getSessionFile();
|
|
126
|
+
|
|
127
|
+
const message = [
|
|
128
|
+
goal,
|
|
129
|
+
...(parent === undefined
|
|
130
|
+
? []
|
|
131
|
+
: ["/skill:session-query", `**Parent session:** \`${parent}\``]),
|
|
132
|
+
summary,
|
|
133
|
+
].join("\n\n");
|
|
134
|
+
|
|
135
|
+
const launch = { message, title: goal.slice(0, TITLE_LENGTH) };
|
|
136
|
+
|
|
137
|
+
const launched = isEmptyChange(change)
|
|
138
|
+
? await callT3Tool("t3_thread_launch", launch)
|
|
139
|
+
: await callT3Tool("t3_thread_launch", {
|
|
140
|
+
...launch,
|
|
141
|
+
modelSelection: { ...applyChange(await currentSelection(), change) },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!isLaunched(launched)) {
|
|
145
|
+
throw new Error("t3_thread_launch returned no thread.");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return launched.link ?? launched.threadId;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const handoffTool = (loadModeSpec: LoadModeSpec) =>
|
|
152
|
+
defineTool({
|
|
153
|
+
description:
|
|
154
|
+
"Transfer context to a new focused T3 thread. ONLY use this when the user explicitly asks for a handoff. Provide a goal describing what the new thread should focus on. The result is the new thread's link, which you show the user verbatim.",
|
|
155
|
+
execute: async (
|
|
156
|
+
_toolCallId,
|
|
157
|
+
params: Static<typeof HandoffSchema>,
|
|
158
|
+
signal,
|
|
159
|
+
_onUpdate,
|
|
160
|
+
ctx
|
|
161
|
+
) => {
|
|
162
|
+
const link = await handoff(
|
|
163
|
+
ctx,
|
|
164
|
+
params.goal,
|
|
165
|
+
{ mode: params.mode, model: params.model },
|
|
166
|
+
loadModeSpec,
|
|
167
|
+
signal
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
content: [{ text: `Handoff thread started: ${link}`, type: "text" }],
|
|
172
|
+
details: { link },
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
label: "Handoff",
|
|
176
|
+
name: "handoff",
|
|
177
|
+
parameters: HandoffSchema,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
export const handoffCommand = (loadModeSpec: LoadModeSpec): Command => ({
|
|
181
|
+
description:
|
|
182
|
+
"Start a new T3 thread with a summary of this one: /handoff [-mode <name>] [-model <provider/id>] <goal>",
|
|
183
|
+
handler: async (args, ctx) => {
|
|
184
|
+
try {
|
|
185
|
+
const { flags, text } = parseFlags(args);
|
|
186
|
+
|
|
187
|
+
if (text.length === 0) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
"Usage: /handoff [-mode <name>] [-model <provider/id>] <goal>"
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const link = await handoff(ctx, text, flags, loadModeSpec, ctx.signal);
|
|
194
|
+
|
|
195
|
+
ctx.ui.notify(`Handoff thread started: ${link}`, "info");
|
|
196
|
+
} catch (error) {
|
|
197
|
+
ctx.ui.notify(`Handoff failed: ${String(error)}`, "error");
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DefaultPackageManager,
|
|
6
|
+
getAgentDir,
|
|
7
|
+
SettingsManager,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import type {
|
|
10
|
+
ExtensionFactory,
|
|
11
|
+
ResolvedResource,
|
|
12
|
+
} from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { compile, obj, opt, str, unknown } from "@earendil-works/pi-schema";
|
|
14
|
+
import type { Static } from "@earendil-works/pi-schema";
|
|
15
|
+
|
|
16
|
+
const PACKAGE_NAME = "pi-amplike";
|
|
17
|
+
|
|
18
|
+
const isManifest = compile(obj({ name: str() }, { open: true })).check;
|
|
19
|
+
|
|
20
|
+
// JSON Schema has no function type, so these check that the export exists and
|
|
21
|
+
// isFunction checks that it is a function. Like TypeBox's Type.Function, the
|
|
22
|
+
// check can't see the function's parameters, so the caller names them.
|
|
23
|
+
const isFunction = <F extends (...args: never[]) => void>(
|
|
24
|
+
value: unknown
|
|
25
|
+
): value is F => typeof value === "function";
|
|
26
|
+
|
|
27
|
+
const isFactoryModule = compile(
|
|
28
|
+
obj({ default: unknown() }, { open: true })
|
|
29
|
+
).check;
|
|
30
|
+
|
|
31
|
+
const isModeUtils = compile(
|
|
32
|
+
obj({ loadModeSpec: unknown() }, { open: true })
|
|
33
|
+
).check;
|
|
34
|
+
|
|
35
|
+
const ModeSpecSchema = obj(
|
|
36
|
+
{
|
|
37
|
+
modelId: opt(str()),
|
|
38
|
+
provider: opt(str()),
|
|
39
|
+
thinkingLevel: opt(str()),
|
|
40
|
+
},
|
|
41
|
+
{ open: true }
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const isModeSpec = compile(ModeSpecSchema).check;
|
|
45
|
+
|
|
46
|
+
export type ModeSpec = Static<typeof ModeSpecSchema>;
|
|
47
|
+
|
|
48
|
+
export type LoadModeSpec = (
|
|
49
|
+
cwd: string,
|
|
50
|
+
name: string
|
|
51
|
+
) => Promise<ModeSpec | undefined>;
|
|
52
|
+
|
|
53
|
+
export interface Amplike {
|
|
54
|
+
readonly extensions: readonly string[];
|
|
55
|
+
readonly root: string;
|
|
56
|
+
// True when Pi loads amplike's extensions itself, so this adapter must not.
|
|
57
|
+
readonly selfLoaded: boolean;
|
|
58
|
+
readonly source: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const isAmplikeRoot = (root: string): boolean => {
|
|
62
|
+
try {
|
|
63
|
+
const manifest: unknown = JSON.parse(
|
|
64
|
+
readFileSync(path.join(root, "package.json"), "utf-8")
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return isManifest(manifest) && manifest.name === PACKAGE_NAME;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const isAmplike = (resource: ResolvedResource): boolean =>
|
|
74
|
+
resource.metadata.packageRoot !== undefined &&
|
|
75
|
+
isAmplikeRoot(resource.metadata.packageRoot);
|
|
76
|
+
|
|
77
|
+
// Pi's own package manager gives the extension files amplike's manifest names,
|
|
78
|
+
// and whether the settings entry for amplike turns them off.
|
|
79
|
+
export const findAmplike = async (
|
|
80
|
+
cwd: string
|
|
81
|
+
): Promise<Amplike | undefined> => {
|
|
82
|
+
const agentDir = getAgentDir();
|
|
83
|
+
|
|
84
|
+
const manager = new DefaultPackageManager({
|
|
85
|
+
agentDir,
|
|
86
|
+
cwd,
|
|
87
|
+
settingsManager: SettingsManager.create(cwd, agentDir),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const resolved = await manager.resolve(async () => "skip");
|
|
91
|
+
const own = resolved.extensions.filter(isAmplike);
|
|
92
|
+
const [first] = own;
|
|
93
|
+
|
|
94
|
+
if (first?.metadata.packageRoot === undefined) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
extensions: own.map((resource) => resource.path),
|
|
100
|
+
root: first.metadata.packageRoot,
|
|
101
|
+
selfLoaded: own.some((resource) => resource.enabled),
|
|
102
|
+
source: first.metadata.source,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const loadFactory = async (file: string): Promise<ExtensionFactory> => {
|
|
107
|
+
const module: unknown = await import(file);
|
|
108
|
+
|
|
109
|
+
if (
|
|
110
|
+
!isFactoryModule(module) ||
|
|
111
|
+
!isFunction<ExtensionFactory>(module.default)
|
|
112
|
+
) {
|
|
113
|
+
throw new TypeError(`${file} has no default extension factory.`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return module.default;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// amplike's handoff and subagent read -mode through this helper, so the T3
|
|
120
|
+
// replacements read it the same way.
|
|
121
|
+
export const loadModeSpecFrom = async (root: string): Promise<LoadModeSpec> => {
|
|
122
|
+
const file = path.join(root, "extensions", "lib", "mode-utils.ts");
|
|
123
|
+
const module: unknown = await import(file);
|
|
124
|
+
|
|
125
|
+
if (!isModeUtils(module) || !isFunction<LoadModeSpec>(module.loadModeSpec)) {
|
|
126
|
+
throw new TypeError(`${file} has no loadModeSpec export.`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const { loadModeSpec } = module;
|
|
130
|
+
|
|
131
|
+
return async (cwd, name) => {
|
|
132
|
+
const spec: unknown = await loadModeSpec(cwd, name);
|
|
133
|
+
|
|
134
|
+
return isModeSpec(spec) ? spec : undefined;
|
|
135
|
+
};
|
|
136
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import {
|
|
2
|
+
arr,
|
|
3
|
+
bool,
|
|
4
|
+
compile,
|
|
5
|
+
obj,
|
|
6
|
+
opt,
|
|
7
|
+
str,
|
|
8
|
+
union,
|
|
9
|
+
} from "@earendil-works/pi-schema";
|
|
10
|
+
import type { Static } from "@earendil-works/pi-schema";
|
|
11
|
+
|
|
12
|
+
import { callT3Tool } from "../bridge/t3-http.ts";
|
|
13
|
+
import type { LoadModeSpec } from "./package.ts";
|
|
14
|
+
|
|
15
|
+
const OptionSchema = obj(
|
|
16
|
+
{ id: str(), value: union([str(), bool()]) },
|
|
17
|
+
{ open: true }
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const SelectionSchema = obj(
|
|
21
|
+
{
|
|
22
|
+
instanceId: opt(str()),
|
|
23
|
+
model: str(),
|
|
24
|
+
options: opt(arr(OptionSchema)),
|
|
25
|
+
},
|
|
26
|
+
{ open: true }
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const isConfigured = compile(
|
|
30
|
+
obj({ modelSelection: SelectionSchema }, { open: true })
|
|
31
|
+
).check;
|
|
32
|
+
|
|
33
|
+
// T3's modelSelection for a Pi thread: model is "provider/modelId", and the
|
|
34
|
+
// thinking level is the "thinking" option.
|
|
35
|
+
export type Selection = Static<typeof SelectionSchema>;
|
|
36
|
+
|
|
37
|
+
export interface SelectionChange {
|
|
38
|
+
model?: string;
|
|
39
|
+
thinking?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface Flags {
|
|
43
|
+
mode?: string;
|
|
44
|
+
model?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ParsedArgs {
|
|
48
|
+
readonly flags: Flags;
|
|
49
|
+
readonly text: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const THINKING = "thinking";
|
|
53
|
+
|
|
54
|
+
export const isEmptyChange = (change: SelectionChange): boolean =>
|
|
55
|
+
change.model === undefined && change.thinking === undefined;
|
|
56
|
+
|
|
57
|
+
export const currentSelection = async (): Promise<Selection> => {
|
|
58
|
+
const result = await callT3Tool("t3_thread_configure", {});
|
|
59
|
+
|
|
60
|
+
if (!isConfigured(result)) {
|
|
61
|
+
throw new Error("t3_thread_configure returned no modelSelection.");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return result.modelSelection;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const applyChange = (
|
|
68
|
+
current: Selection,
|
|
69
|
+
change: SelectionChange
|
|
70
|
+
): Selection => {
|
|
71
|
+
const next: Selection = { ...current, model: change.model ?? current.model };
|
|
72
|
+
|
|
73
|
+
if (change.thinking !== undefined) {
|
|
74
|
+
next.options = [
|
|
75
|
+
...(current.options ?? []).filter((option) => option.id !== THINKING),
|
|
76
|
+
{ id: THINKING, value: change.thinking },
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return next;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const configureThread = async (selection: Selection): Promise<void> => {
|
|
84
|
+
await callT3Tool("t3_thread_configure", { modelSelection: { ...selection } });
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// amplike's commands take `-mode <name>` and `-model <provider/id>` before the
|
|
88
|
+
// text, in either order.
|
|
89
|
+
export const parseFlags = (args: string): ParsedArgs => {
|
|
90
|
+
const words = args.trim().split(/\s+/u);
|
|
91
|
+
const flags: Flags = {};
|
|
92
|
+
|
|
93
|
+
while (words[0] === "-mode" || words[0] === "-model") {
|
|
94
|
+
const [flag, value] = words.splice(0, 2);
|
|
95
|
+
|
|
96
|
+
if (value === undefined) {
|
|
97
|
+
throw new Error(`${flag} needs a value.`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (flag === "-mode") {
|
|
101
|
+
flags.mode = value;
|
|
102
|
+
} else {
|
|
103
|
+
flags.model = value;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { flags, text: words.join(" ") };
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Same order as amplike: the mode sets model and thinking, then -model
|
|
111
|
+
// overrides the model.
|
|
112
|
+
export const changeFromFlags = async (
|
|
113
|
+
flags: Flags,
|
|
114
|
+
cwd: string,
|
|
115
|
+
loadModeSpec: LoadModeSpec
|
|
116
|
+
): Promise<SelectionChange> => {
|
|
117
|
+
const change: SelectionChange = {};
|
|
118
|
+
|
|
119
|
+
if (flags.mode !== undefined) {
|
|
120
|
+
const spec = await loadModeSpec(cwd, flags.mode);
|
|
121
|
+
|
|
122
|
+
if (spec === undefined) {
|
|
123
|
+
throw new Error(`Unknown mode "${flags.mode}".`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (spec.provider !== undefined && spec.modelId !== undefined) {
|
|
127
|
+
change.model = `${spec.provider}/${spec.modelId}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
change.thinking = spec.thinkingLevel;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (flags.model !== undefined) {
|
|
134
|
+
if (!flags.model.includes("/")) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Model "${flags.model}" must be written as provider/modelId.`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
change.model = flags.model;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return change;
|
|
144
|
+
};
|