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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import { btwCommand } from "./btw.ts";
|
|
4
|
+
import { handoffCommand, handoffTool } from "./handoff.ts";
|
|
5
|
+
import type { LoadModeSpec } from "./package.ts";
|
|
6
|
+
import { applyChange, configureThread, currentSelection } from "./selection.ts";
|
|
7
|
+
import type { SelectionChange } from "./selection.ts";
|
|
8
|
+
|
|
9
|
+
type Tool = Parameters<ExtensionAPI["registerTool"]>[0];
|
|
10
|
+
|
|
11
|
+
type Command = Parameters<ExtensionAPI["registerCommand"]>[1];
|
|
12
|
+
|
|
13
|
+
// What a T3 thread gets for each name amplike registers. A missing name is
|
|
14
|
+
// registered as amplike wrote it. null drops it.
|
|
15
|
+
interface Rules {
|
|
16
|
+
readonly commands: Readonly<Record<string, Command | null>>;
|
|
17
|
+
readonly tools: Readonly<Record<string, Tool | null>>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const t3Rules = (loadModeSpec: LoadModeSpec): Rules => ({
|
|
21
|
+
commands: {
|
|
22
|
+
btw: btwCommand(loadModeSpec),
|
|
23
|
+
handoff: handoffCommand(loadModeSpec),
|
|
24
|
+
},
|
|
25
|
+
tools: {
|
|
26
|
+
handoff: handoffTool(loadModeSpec),
|
|
27
|
+
// T3's delegate_task runs subagents as T3 threads.
|
|
28
|
+
subagent: null,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// T3 owns the thread's model. When amplike's /mode switches Pi's model or
|
|
33
|
+
// thinking level, the same change goes to T3, so T3's model picker matches and
|
|
34
|
+
// T3's next set_model keeps it. amplike awaits setModel before it sets the
|
|
35
|
+
// thinking level, so the two calls reach T3 in order.
|
|
36
|
+
const mirror = async (change: SelectionChange): Promise<void> => {
|
|
37
|
+
await configureThread(applyChange(await currentSelection(), change));
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const mirrorThinking = async (thinking: string): Promise<void> => {
|
|
41
|
+
try {
|
|
42
|
+
await mirror({ thinking });
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error(`pi-t3: T3 kept its thinking level: ${String(error)}`);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Pi's extension API is a plain object of closures, so a copy with some
|
|
49
|
+
// methods replaced behaves like the original for everything else.
|
|
50
|
+
export const t3View = (
|
|
51
|
+
pi: ExtensionAPI,
|
|
52
|
+
loadModeSpec: LoadModeSpec
|
|
53
|
+
): ExtensionAPI => {
|
|
54
|
+
const rules = t3Rules(loadModeSpec);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
...pi,
|
|
58
|
+
registerCommand: (name, options) => {
|
|
59
|
+
const rule = rules.commands[name];
|
|
60
|
+
|
|
61
|
+
if (rule !== null) {
|
|
62
|
+
pi.registerCommand(name, rule ?? options);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
// T3 sends Pi no key presses.
|
|
66
|
+
registerShortcut: () => null,
|
|
67
|
+
registerTool: (tool) => {
|
|
68
|
+
const rule = rules.tools[tool.name];
|
|
69
|
+
|
|
70
|
+
if (rule !== null) {
|
|
71
|
+
pi.registerTool(rule ?? tool);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
setModel: async (model) => {
|
|
75
|
+
const ok = await pi.setModel(model);
|
|
76
|
+
|
|
77
|
+
if (ok) {
|
|
78
|
+
await mirror({ model: `${model.provider}/${model.id}` });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return ok;
|
|
82
|
+
},
|
|
83
|
+
setThinkingLevel: (level) => {
|
|
84
|
+
pi.setThinkingLevel(level);
|
|
85
|
+
void mirrorThinking(level);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
};
|
package/src/bridge/accounts.ts
CHANGED
|
@@ -3,9 +3,19 @@ import type {
|
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
ExtensionToolContext,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import {
|
|
7
|
+
arr,
|
|
8
|
+
bool,
|
|
9
|
+
compile,
|
|
10
|
+
lit,
|
|
11
|
+
nullable,
|
|
12
|
+
num,
|
|
13
|
+
obj,
|
|
14
|
+
opt,
|
|
15
|
+
str,
|
|
16
|
+
union,
|
|
17
|
+
} from "@earendil-works/pi-schema";
|
|
18
|
+
import type { Static } from "@earendil-works/pi-schema";
|
|
9
19
|
|
|
10
20
|
import { classifyFailure } from "./failures.ts";
|
|
11
21
|
import type { Classified } from "./failures.ts";
|
|
@@ -62,15 +72,16 @@ const NO_RESET_MS = 60 * 60_000;
|
|
|
62
72
|
|
|
63
73
|
const USAGE_LIMIT_TITLE = "Usage limit reached";
|
|
64
74
|
|
|
65
|
-
const AccountEventSchema =
|
|
66
|
-
|
|
67
|
-
childThreadId:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}),
|
|
71
|
-
Type.Object({ childThreadId: Type.String(), kind: Type.Literal("settled") }),
|
|
75
|
+
const AccountEventSchema = union([
|
|
76
|
+
obj(
|
|
77
|
+
{ childThreadId: str(), family: str(), kind: lit("delegated") },
|
|
78
|
+
{ open: true }
|
|
79
|
+
),
|
|
80
|
+
obj({ childThreadId: str(), kind: lit("settled") }, { open: true }),
|
|
72
81
|
]);
|
|
73
82
|
|
|
83
|
+
const isAccountEvent = compile(AccountEventSchema).check;
|
|
84
|
+
|
|
74
85
|
type AccountEvent = Static<typeof AccountEventSchema>;
|
|
75
86
|
|
|
76
87
|
export interface Accounts {
|
|
@@ -100,7 +111,7 @@ export const readAccounts = (ctx: ExtensionContext): Accounts => {
|
|
|
100
111
|
if (
|
|
101
112
|
entry.type === "custom" &&
|
|
102
113
|
entry.customType === ACCOUNT_ENTRY &&
|
|
103
|
-
|
|
114
|
+
isAccountEvent(entry.data)
|
|
104
115
|
) {
|
|
105
116
|
const event: AccountEvent = entry.data;
|
|
106
117
|
|
|
@@ -186,19 +197,23 @@ export const recordFailure = (
|
|
|
186
197
|
updateFamily(modelFamily, record);
|
|
187
198
|
};
|
|
188
199
|
|
|
189
|
-
const ThreadReadSchema =
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
200
|
+
const ThreadReadSchema = obj(
|
|
201
|
+
{
|
|
202
|
+
hasMore: bool(),
|
|
203
|
+
items: arr(
|
|
204
|
+
obj(
|
|
205
|
+
{ text: opt(nullable(str())), title: nullable(str()), type: str() },
|
|
206
|
+
{ open: true }
|
|
207
|
+
)
|
|
208
|
+
),
|
|
209
|
+
nextPosition: nullable(num()),
|
|
210
|
+
recentRuns: opt(arr(obj({ status: str() }, { open: true }))),
|
|
211
|
+
thread: obj({ status: str() }, { open: true }),
|
|
212
|
+
},
|
|
213
|
+
{ open: true }
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const threadRead = compile(ThreadReadSchema);
|
|
202
217
|
|
|
203
218
|
type ThreadRead = Static<typeof ThreadReadSchema>;
|
|
204
219
|
|
|
@@ -217,16 +232,14 @@ const bridgeReader =
|
|
|
217
232
|
async (params) => {
|
|
218
233
|
const outcome = await ctx.executeTool(toolName, params);
|
|
219
234
|
|
|
220
|
-
return outcome.isError
|
|
221
|
-
? undefined
|
|
222
|
-
: t3Json(ThreadReadSchema, outcome.result);
|
|
235
|
+
return outcome.isError ? undefined : t3Json(threadRead, outcome.result);
|
|
223
236
|
};
|
|
224
237
|
|
|
225
238
|
// A tool_result handler gets no ctx.executeTool, so it reads through T3 directly.
|
|
226
239
|
export const httpReader: PageReader = async (params) => {
|
|
227
240
|
const read = await callT3Tool("t3_thread_read", { ...params });
|
|
228
241
|
|
|
229
|
-
return
|
|
242
|
+
return threadRead.check(read) ? read : undefined;
|
|
230
243
|
};
|
|
231
244
|
|
|
232
245
|
type ChildOutcome =
|
|
@@ -358,27 +371,33 @@ const STATUS_TOOLS: ReadonlySet<string> = new Set([
|
|
|
358
371
|
"mcp__t3__task_status",
|
|
359
372
|
]);
|
|
360
373
|
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
374
|
+
const taskStatus = compile(
|
|
375
|
+
obj(
|
|
376
|
+
{ childThreadId: str(), status: opt(str()), workState: opt(str()) },
|
|
377
|
+
{ open: true }
|
|
378
|
+
)
|
|
379
|
+
);
|
|
366
380
|
|
|
367
|
-
const
|
|
368
|
-
target:
|
|
369
|
-
|
|
381
|
+
const isDelegateInput = compile(
|
|
382
|
+
obj({ target: opt(obj({ model: str() }, { open: true })) }, { open: true })
|
|
383
|
+
).check;
|
|
370
384
|
|
|
371
|
-
const
|
|
385
|
+
const delegateResult = compile(obj({ childThreadId: str() }, { open: true }));
|
|
372
386
|
|
|
373
387
|
const sessionFamily = (ctx: ExtensionContext): string | undefined =>
|
|
374
388
|
ctx.model === undefined ? undefined : family(ctx.model.id);
|
|
375
389
|
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
390
|
+
const isAssistantError = compile(
|
|
391
|
+
obj(
|
|
392
|
+
{
|
|
393
|
+
errorMessage: opt(str()),
|
|
394
|
+
model: str(),
|
|
395
|
+
role: lit("assistant"),
|
|
396
|
+
stopReason: str(),
|
|
397
|
+
},
|
|
398
|
+
{ open: true }
|
|
399
|
+
)
|
|
400
|
+
).check;
|
|
382
401
|
|
|
383
402
|
// Recovery takes any family accounts.json records, such as kimi. Marking one
|
|
384
403
|
// out takes only a family of a model Pi knows, so a typo cannot add one.
|
|
@@ -410,7 +429,7 @@ export const registerAccounts = (pi: ExtensionAPI): void => {
|
|
|
410
429
|
return;
|
|
411
430
|
}
|
|
412
431
|
|
|
413
|
-
const status = t3Json(
|
|
432
|
+
const status = t3Json(taskStatus, event);
|
|
414
433
|
|
|
415
434
|
const childFamily =
|
|
416
435
|
status === undefined
|
|
@@ -437,11 +456,9 @@ export const registerAccounts = (pi: ExtensionAPI): void => {
|
|
|
437
456
|
return;
|
|
438
457
|
}
|
|
439
458
|
|
|
440
|
-
const result = t3Json(
|
|
459
|
+
const result = t3Json(delegateResult, event);
|
|
441
460
|
|
|
442
|
-
const input =
|
|
443
|
-
? event.input
|
|
444
|
-
: undefined;
|
|
461
|
+
const input = isDelegateInput(event.input) ? event.input : undefined;
|
|
445
462
|
|
|
446
463
|
const childFamily =
|
|
447
464
|
input?.target === undefined
|
|
@@ -493,7 +510,7 @@ export const registerAccounts = (pi: ExtensionAPI): void => {
|
|
|
493
510
|
(message) => (message as { role?: string }).role === "assistant"
|
|
494
511
|
);
|
|
495
512
|
|
|
496
|
-
if (!
|
|
513
|
+
if (!isAssistantError(last)) {
|
|
497
514
|
lastRun = undefined;
|
|
498
515
|
} else if (last.stopReason === "error") {
|
|
499
516
|
lastRun = {
|
package/src/bridge/ask.ts
CHANGED
|
@@ -4,103 +4,79 @@ import type {
|
|
|
4
4
|
ExtensionUIContext,
|
|
5
5
|
ExtensionUIDialogOptions,
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import {
|
|
8
|
+
arr,
|
|
9
|
+
compile,
|
|
10
|
+
int,
|
|
11
|
+
lit,
|
|
12
|
+
obj,
|
|
13
|
+
oneOf,
|
|
14
|
+
opt,
|
|
15
|
+
str,
|
|
16
|
+
union,
|
|
17
|
+
} from "@earendil-works/pi-schema";
|
|
18
|
+
import type { Static } from "@earendil-works/pi-schema";
|
|
10
19
|
|
|
11
20
|
import { registerUnlessTaken } from "./claim.ts";
|
|
12
21
|
import { insideT3 } from "./t3-http.ts";
|
|
13
22
|
|
|
14
23
|
const OWN_ANSWER = "Type my own answer";
|
|
15
24
|
|
|
16
|
-
const ChoiceSchema =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
},
|
|
26
|
-
{ additionalProperties: false }
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
const QuestionSchema = Type.Object(
|
|
30
|
-
{
|
|
31
|
-
body: Type.Optional(
|
|
32
|
-
Type.String({
|
|
33
|
-
description: "More context, shown under the title.",
|
|
34
|
-
minLength: 1,
|
|
35
|
-
})
|
|
36
|
-
),
|
|
37
|
-
choices: Type.Optional(
|
|
38
|
-
Type.Array(ChoiceSchema, {
|
|
39
|
-
description: "Choices to pick from. Leave out for free text.",
|
|
40
|
-
minItems: 1,
|
|
41
|
-
})
|
|
42
|
-
),
|
|
43
|
-
title: Type.String({ description: "The question.", minLength: 1 }),
|
|
44
|
-
},
|
|
45
|
-
{ additionalProperties: false }
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
const AskInputSchema = Type.Object(
|
|
49
|
-
{
|
|
50
|
-
questions: Type.Array(QuestionSchema, {
|
|
51
|
-
description: "Questions to ask in order, one card each.",
|
|
52
|
-
minItems: 1,
|
|
53
|
-
}),
|
|
54
|
-
timeoutSeconds: Type.Optional(
|
|
55
|
-
Type.Integer({
|
|
56
|
-
description: "Seconds to wait for all answers before giving up.",
|
|
57
|
-
minimum: 10,
|
|
58
|
-
})
|
|
59
|
-
),
|
|
60
|
-
},
|
|
61
|
-
{ additionalProperties: false }
|
|
62
|
-
);
|
|
25
|
+
const ChoiceSchema = obj({
|
|
26
|
+
description: opt(
|
|
27
|
+
str({
|
|
28
|
+
description: "Why to pick it, shown under the question.",
|
|
29
|
+
minLength: 1,
|
|
30
|
+
})
|
|
31
|
+
),
|
|
32
|
+
label: str({ description: "The choice.", minLength: 1 }),
|
|
33
|
+
});
|
|
63
34
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
{
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
question: Type.Integer(),
|
|
71
|
-
},
|
|
72
|
-
{ additionalProperties: false }
|
|
35
|
+
const QuestionSchema = obj({
|
|
36
|
+
body: opt(
|
|
37
|
+
str({
|
|
38
|
+
description: "More context, shown under the title.",
|
|
39
|
+
minLength: 1,
|
|
40
|
+
})
|
|
73
41
|
),
|
|
74
|
-
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
},
|
|
80
|
-
{ additionalProperties: false }
|
|
42
|
+
choices: opt(
|
|
43
|
+
arr(ChoiceSchema, {
|
|
44
|
+
description: "Choices to pick from. Leave out for free text.",
|
|
45
|
+
minItems: 1,
|
|
46
|
+
})
|
|
81
47
|
),
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
48
|
+
title: str({ description: "The question.", minLength: 1 }),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const AskInputSchema = obj({
|
|
52
|
+
questions: arr(QuestionSchema, {
|
|
53
|
+
description: "Questions to ask in order, one card each.",
|
|
54
|
+
minItems: 1,
|
|
55
|
+
}),
|
|
56
|
+
timeoutSeconds: opt(
|
|
57
|
+
int({
|
|
58
|
+
description: "Seconds to wait for all answers before giving up.",
|
|
59
|
+
minimum: 10,
|
|
60
|
+
})
|
|
85
61
|
),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const AnswerSchema = union([
|
|
65
|
+
obj({ index: int(), kind: lit("chose"), label: str(), question: int() }),
|
|
66
|
+
obj({ kind: lit("typed"), question: int(), text: str() }),
|
|
67
|
+
obj({ kind: lit("rephrase"), question: int() }),
|
|
86
68
|
]);
|
|
87
69
|
|
|
88
|
-
const StopSchema =
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
},
|
|
94
|
-
{ additionalProperties: false }
|
|
95
|
-
);
|
|
70
|
+
const StopSchema = obj({
|
|
71
|
+
kind: oneOf(["dismissed", "timed_out"]),
|
|
72
|
+
question: int(),
|
|
73
|
+
unasked: int(),
|
|
74
|
+
});
|
|
96
75
|
|
|
97
|
-
const AskDetailsSchema =
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
},
|
|
102
|
-
{ additionalProperties: false }
|
|
103
|
-
);
|
|
76
|
+
const AskDetailsSchema = obj({
|
|
77
|
+
answers: arr(AnswerSchema),
|
|
78
|
+
stop: opt(StopSchema),
|
|
79
|
+
});
|
|
104
80
|
|
|
105
81
|
type Question = Static<typeof QuestionSchema>;
|
|
106
82
|
|
|
@@ -115,15 +91,19 @@ type AskDetails = Static<typeof AskDetailsSchema>;
|
|
|
115
91
|
type Surface = "t3" | "terminal";
|
|
116
92
|
|
|
117
93
|
// The call as the model sent it, before AskInputSchema rejects extra fields.
|
|
118
|
-
const RawChoiceSchema =
|
|
94
|
+
const RawChoiceSchema = obj({}, { open: true });
|
|
119
95
|
|
|
120
|
-
const RawQuestionSchema =
|
|
121
|
-
choices:
|
|
122
|
-
}
|
|
96
|
+
const RawQuestionSchema = obj(
|
|
97
|
+
{ choices: opt(arr(RawChoiceSchema)) },
|
|
98
|
+
{ open: true }
|
|
99
|
+
);
|
|
123
100
|
|
|
124
|
-
const RawAskSchema =
|
|
125
|
-
questions:
|
|
126
|
-
}
|
|
101
|
+
const RawAskSchema = obj(
|
|
102
|
+
{ questions: opt(arr(RawQuestionSchema)) },
|
|
103
|
+
{ open: true }
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const isRawAsk = compile(RawAskSchema).check;
|
|
127
107
|
|
|
128
108
|
type RawCall =
|
|
129
109
|
| Static<typeof RawAskSchema>
|
|
@@ -294,7 +274,7 @@ const askTool = defineTool({
|
|
|
294
274
|
annotations: { readOnlyHint: true },
|
|
295
275
|
description:
|
|
296
276
|
'Ask the user and wait for the answer. Each question becomes one card, asked in order. Put your recommendation first and mark it in the label, e.g. "SQLite (recommended)". Put the reason in `description`, which is shown under the question. The user can always type their own answer instead of picking. To let them pick several, say so in `body`. They will type something like "1 and 3" or "all". Leave out `choices` for free text. If a typed answer is a question or pushback, reply to it before you act. If the user dismisses a card, stop and wait for their next message.',
|
|
297
|
-
execute: async (_toolCallId, params, signal, _onUpdate, ctx) => {
|
|
277
|
+
execute: async (_toolCallId, params: AskInput, signal, _onUpdate, ctx) => {
|
|
298
278
|
if (!ctx.hasUI) {
|
|
299
279
|
throw new Error(
|
|
300
280
|
"ask needs an interactive client, such as T3 Code or the Pi terminal."
|
|
@@ -373,7 +353,7 @@ const askTool = defineTool({
|
|
|
373
353
|
outputSchema: AskDetailsSchema,
|
|
374
354
|
parameters: AskInputSchema,
|
|
375
355
|
prepareArguments: (args) => {
|
|
376
|
-
if (
|
|
356
|
+
if (isRawAsk(args)) {
|
|
377
357
|
checkFields(args);
|
|
378
358
|
}
|
|
379
359
|
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
compile,
|
|
4
|
+
num,
|
|
5
|
+
obj,
|
|
6
|
+
opt,
|
|
7
|
+
str,
|
|
8
|
+
unknown,
|
|
9
|
+
} from "@earendil-works/pi-schema";
|
|
4
10
|
|
|
5
11
|
import { readConfig } from "./config.ts";
|
|
6
12
|
|
|
@@ -13,10 +19,11 @@ const DEFAULT_SECONDS = 300;
|
|
|
13
19
|
export const bashTimeoutSeconds = (): number =>
|
|
14
20
|
readConfig().bashTimeoutSeconds ?? DEFAULT_SECONDS;
|
|
15
21
|
|
|
16
|
-
const
|
|
17
|
-
command:
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
const isBashInput = compile(
|
|
23
|
+
obj({ command: str(), timeout: opt(unknown()) }, { open: true })
|
|
24
|
+
).check;
|
|
25
|
+
|
|
26
|
+
const isNumber = compile(num()).check;
|
|
20
27
|
|
|
21
28
|
const TIMED_OUT = /Command timed out after \d+ seconds/u;
|
|
22
29
|
|
|
@@ -24,14 +31,14 @@ export const registerBashTimeout = (pi: ExtensionAPI): void => {
|
|
|
24
31
|
pi.on("tool_call", (event) => {
|
|
25
32
|
const { input } = event;
|
|
26
33
|
|
|
27
|
-
if (event.toolName !== "bash" || !
|
|
34
|
+
if (event.toolName !== "bash" || !isBashInput(input)) {
|
|
28
35
|
return;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
const cap = bashTimeoutSeconds();
|
|
32
39
|
|
|
33
40
|
input.timeout =
|
|
34
|
-
|
|
41
|
+
isNumber(input.timeout) && input.timeout > 0
|
|
35
42
|
? Math.min(input.timeout, cap)
|
|
36
43
|
: cap;
|
|
37
44
|
});
|
package/src/bridge/config.ts
CHANGED
|
@@ -3,24 +3,33 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import {
|
|
7
|
+
compile,
|
|
8
|
+
map,
|
|
9
|
+
num,
|
|
10
|
+
oneOf,
|
|
11
|
+
str,
|
|
12
|
+
unknown,
|
|
13
|
+
} from "@earendil-works/pi-schema";
|
|
14
|
+
import type { Static, Validator } from "@earendil-works/pi-schema";
|
|
9
15
|
|
|
10
16
|
// The bridge's settings, one JSON file per machine. Each key is checked on its
|
|
11
17
|
// own, so one bad value falls back to its default and leaves the rest alone.
|
|
12
18
|
// The file is read on every call, so an edit applies on the next prompt.
|
|
13
19
|
|
|
14
|
-
const SteerSchema =
|
|
15
|
-
Type.Literal("on"),
|
|
16
|
-
Type.Literal("off"),
|
|
17
|
-
Type.Literal("shadow"),
|
|
18
|
-
]);
|
|
20
|
+
const SteerSchema = oneOf(["on", "off", "shadow"]);
|
|
19
21
|
|
|
20
|
-
const ChildSandboxSchema =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const ChildSandboxSchema = oneOf(["on", "off"]);
|
|
23
|
+
|
|
24
|
+
const configFile = compile(map(unknown()));
|
|
25
|
+
|
|
26
|
+
const fields = {
|
|
27
|
+
bashTimeoutSeconds: compile(num({ exclusiveMinimum: 0 })),
|
|
28
|
+
childSandbox: compile(ChildSandboxSchema),
|
|
29
|
+
classifierModel: compile(str({ pattern: "^[^/\\s]+/.+$" })),
|
|
30
|
+
mcpUrl: compile(str({ minLength: 1 })),
|
|
31
|
+
steer: compile(SteerSchema),
|
|
32
|
+
};
|
|
24
33
|
|
|
25
34
|
export interface Config {
|
|
26
35
|
readonly bashTimeoutSeconds: number | undefined;
|
|
@@ -60,19 +69,16 @@ const parse = (text: string): Loaded => {
|
|
|
60
69
|
return { config: EMPTY, problems: [`${file} is not valid JSON.`] };
|
|
61
70
|
}
|
|
62
71
|
|
|
63
|
-
if (!
|
|
72
|
+
if (!configFile.check(parsed)) {
|
|
64
73
|
return { config: EMPTY, problems: [`${file} is not a JSON object.`] };
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
const problems: string[] = [];
|
|
68
77
|
|
|
69
|
-
const field = <
|
|
70
|
-
key: string,
|
|
71
|
-
schema: S
|
|
72
|
-
): Static<S> | undefined => {
|
|
78
|
+
const field = <T>(key: string, validator: Validator<T>): T | undefined => {
|
|
73
79
|
const value = parsed[key];
|
|
74
80
|
|
|
75
|
-
if (value === undefined ||
|
|
81
|
+
if (value === undefined || validator.check(value)) {
|
|
76
82
|
return value;
|
|
77
83
|
}
|
|
78
84
|
|
|
@@ -87,15 +93,12 @@ const parse = (text: string): Loaded => {
|
|
|
87
93
|
config: {
|
|
88
94
|
bashTimeoutSeconds: field(
|
|
89
95
|
"bashTimeoutSeconds",
|
|
90
|
-
|
|
91
|
-
),
|
|
92
|
-
childSandbox: field("childSandbox", ChildSandboxSchema),
|
|
93
|
-
classifierModel: field(
|
|
94
|
-
"classifierModel",
|
|
95
|
-
Type.String({ pattern: "^[^/\\s]+/.+$" })
|
|
96
|
+
fields.bashTimeoutSeconds
|
|
96
97
|
),
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
childSandbox: field("childSandbox", fields.childSandbox),
|
|
99
|
+
classifierModel: field("classifierModel", fields.classifierModel),
|
|
100
|
+
mcpUrl: field("mcpUrl", fields.mcpUrl),
|
|
101
|
+
steer: field("steer", fields.steer),
|
|
99
102
|
},
|
|
100
103
|
problems,
|
|
101
104
|
};
|
package/src/bridge/plan.ts
CHANGED
|
@@ -3,7 +3,8 @@ import type {
|
|
|
3
3
|
ExtensionAPI,
|
|
4
4
|
ExtensionContext,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import {
|
|
6
|
+
import { obj, opt, str } from "@earendil-works/pi-schema";
|
|
7
|
+
import type { Static } from "@earendil-works/pi-schema";
|
|
7
8
|
|
|
8
9
|
import {
|
|
9
10
|
escapeHtml,
|
|
@@ -117,15 +118,16 @@ const planCard = (title: string, plan: string): PlanCard => {
|
|
|
117
118
|
};
|
|
118
119
|
};
|
|
119
120
|
|
|
120
|
-
const PlanSchema =
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
121
|
+
const PlanSchema = obj(
|
|
122
|
+
{
|
|
123
|
+
plan: str({
|
|
124
|
+
description: "The plan in Markdown, as numbered steps.",
|
|
125
|
+
minLength: 1,
|
|
126
|
+
}),
|
|
127
|
+
title: opt(str({ description: "Short plan title.", minLength: 1 })),
|
|
128
|
+
},
|
|
129
|
+
{ open: true }
|
|
130
|
+
);
|
|
129
131
|
|
|
130
132
|
interface PlanToolDetails {
|
|
131
133
|
readonly choice: PlanChoice | "dismissed";
|
|
@@ -177,7 +179,13 @@ export const registerPlanGate = (pi: ExtensionAPI): void => {
|
|
|
177
179
|
annotations: { readOnlyHint: true },
|
|
178
180
|
description:
|
|
179
181
|
"Ask the user to approve a plan before any file changes. Until the user approves, only read-only tools run. On Revise, change the plan and call this tool again. On Cancel, stop: only read-only tools run until the user's next message.",
|
|
180
|
-
execute: async (
|
|
182
|
+
execute: async (
|
|
183
|
+
toolCallId,
|
|
184
|
+
params: Static<typeof PlanSchema>,
|
|
185
|
+
_signal,
|
|
186
|
+
_onUpdate,
|
|
187
|
+
ctx
|
|
188
|
+
) => {
|
|
181
189
|
if (!ctx.hasUI) {
|
|
182
190
|
throw new Error(
|
|
183
191
|
"Plan approval needs an interactive client, such as T3 Code or the Pi terminal."
|