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.
Files changed (98) hide show
  1. package/README.md +20 -0
  2. package/amplike.ts +46 -0
  3. package/package.json +7 -6
  4. package/src/amplike/btw.ts +89 -0
  5. package/src/amplike/handoff.ts +200 -0
  6. package/src/amplike/package.ts +136 -0
  7. package/src/amplike/selection.ts +144 -0
  8. package/src/amplike/t3-view.ts +88 -0
  9. package/src/bridge/accounts.ts +66 -49
  10. package/src/bridge/ask.ts +73 -93
  11. package/src/bridge/bash-timeout.ts +15 -8
  12. package/src/bridge/config.ts +29 -26
  13. package/src/bridge/plan.ts +19 -11
  14. package/src/bridge/resume.ts +17 -11
  15. package/src/bridge/state.ts +54 -34
  16. package/src/bridge/steer-judge.ts +130 -96
  17. package/src/bridge/steer-transcript.ts +4 -3
  18. package/src/bridge/steer.ts +16 -10
  19. package/src/bridge/t3-json.ts +14 -19
  20. package/src/bridge/tasks.ts +39 -31
  21. package/src/bridge/thread.ts +5 -6
  22. package/src/bridge/transcript.ts +11 -8
  23. package/src/pstack/brief.ts +4 -3
  24. package/src/pstack/child.ts +45 -32
  25. package/src/pstack/models.ts +138 -106
  26. package/src/pstack/overlay.ts +18 -20
  27. package/src/pstack/results.ts +4 -7
  28. package/src/pstack/sandbox.ts +12 -8
  29. package/pstack/skills/statechart/scripts/node_modules/.package-lock.json +0 -46
  30. package/pstack/skills/statechart/scripts/node_modules/fast-check/LICENSE +0 -21
  31. package/pstack/skills/statechart/scripts/node_modules/fast-check/README.md +0 -263
  32. package/pstack/skills/statechart/scripts/node_modules/fast-check/lib/cjs/fast-check.d.ts +0 -5165
  33. package/pstack/skills/statechart/scripts/node_modules/fast-check/lib/cjs/fast-check.js +0 -12147
  34. package/pstack/skills/statechart/scripts/node_modules/fast-check/lib/cjs/package.json +0 -3
  35. package/pstack/skills/statechart/scripts/node_modules/fast-check/lib/cjs/types57/fast-check.d.ts +0 -5165
  36. package/pstack/skills/statechart/scripts/node_modules/fast-check/lib/fast-check.d.ts +0 -5165
  37. package/pstack/skills/statechart/scripts/node_modules/fast-check/lib/fast-check.js +0 -12011
  38. package/pstack/skills/statechart/scripts/node_modules/fast-check/lib/rolldown-runtime-D7D4PA-g.js +0 -13
  39. package/pstack/skills/statechart/scripts/node_modules/fast-check/lib/types57/fast-check.d.ts +0 -5165
  40. package/pstack/skills/statechart/scripts/node_modules/fast-check/package.json +0 -94
  41. package/pstack/skills/statechart/scripts/node_modules/pure-rand/LICENSE +0 -21
  42. package/pstack/skills/statechart/scripts/node_modules/pure-rand/README.md +0 -168
  43. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/RandomGenerator-DcXj09Ch.d.ts +0 -14
  44. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/distribution/uniformBigInt.d.ts +0 -15
  45. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/distribution/uniformBigInt.js +0 -38
  46. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/distribution/uniformFloat32.d.ts +0 -15
  47. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/distribution/uniformFloat32.js +0 -18
  48. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/distribution/uniformFloat64.d.ts +0 -15
  49. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/distribution/uniformFloat64.js +0 -22
  50. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/distribution/uniformInt.d.ts +0 -15
  51. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/distribution/uniformInt.js +0 -134
  52. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/RandomGenerator-DcXj09Ch.d.ts +0 -14
  53. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/distribution/uniformBigInt.d.ts +0 -15
  54. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/distribution/uniformBigInt.js +0 -37
  55. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/distribution/uniformFloat32.d.ts +0 -15
  56. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/distribution/uniformFloat32.js +0 -17
  57. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/distribution/uniformFloat64.d.ts +0 -15
  58. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/distribution/uniformFloat64.js +0 -21
  59. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/distribution/uniformInt.d.ts +0 -15
  60. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/distribution/uniformInt.js +0 -133
  61. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/generator/congruential32.d.ts +0 -7
  62. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/generator/congruential32.js +0 -44
  63. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/generator/mersenne.d.ts +0 -7
  64. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/generator/mersenne.js +0 -90
  65. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/generator/xoroshiro128plus.d.ts +0 -7
  66. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/generator/xoroshiro128plus.js +0 -80
  67. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/generator/xorshift128plus.d.ts +0 -7
  68. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/generator/xorshift128plus.js +0 -78
  69. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/package.json +0 -3
  70. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/types/JumpableRandomGenerator.d.ts +0 -16
  71. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/types/JumpableRandomGenerator.js +0 -0
  72. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/types/RandomGenerator.d.ts +0 -2
  73. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/types/RandomGenerator.js +0 -0
  74. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/utils/generateN.d.ts +0 -6
  75. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/utils/generateN.js +0 -8
  76. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/utils/purify.d.ts +0 -12
  77. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/utils/purify.js +0 -9
  78. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/utils/skipN.d.ts +0 -6
  79. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/esm/utils/skipN.js +0 -6
  80. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/generator/congruential32.d.ts +0 -7
  81. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/generator/congruential32.js +0 -46
  82. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/generator/mersenne.d.ts +0 -7
  83. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/generator/mersenne.js +0 -92
  84. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/generator/xoroshiro128plus.d.ts +0 -7
  85. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/generator/xoroshiro128plus.js +0 -82
  86. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/generator/xorshift128plus.d.ts +0 -7
  87. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/generator/xorshift128plus.js +0 -80
  88. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/types/JumpableRandomGenerator.d.ts +0 -16
  89. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/types/JumpableRandomGenerator.js +0 -0
  90. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/types/RandomGenerator.d.ts +0 -2
  91. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/types/RandomGenerator.js +0 -0
  92. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/utils/generateN.d.ts +0 -6
  93. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/utils/generateN.js +0 -9
  94. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/utils/purify.d.ts +0 -12
  95. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/utils/purify.js +0 -10
  96. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/utils/skipN.d.ts +0 -6
  97. package/pstack/skills/statechart/scripts/node_modules/pure-rand/lib/utils/skipN.js +0 -7
  98. 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
+ };
@@ -3,9 +3,19 @@ import type {
3
3
  ExtensionContext,
4
4
  ExtensionToolContext,
5
5
  } from "@earendil-works/pi-coding-agent";
6
- import { Type } from "typebox";
7
- import type { Static } from "typebox";
8
- import { Value } from "typebox/value";
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 = Type.Union([
66
- Type.Object({
67
- childThreadId: Type.String(),
68
- family: Type.String(),
69
- kind: Type.Literal("delegated"),
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
- Value.Check(AccountEventSchema, entry.data)
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 = Type.Object({
190
- hasMore: Type.Boolean(),
191
- items: Type.Array(
192
- Type.Object({
193
- text: Type.Optional(Type.Union([Type.String(), Type.Null()])),
194
- title: Type.Union([Type.String(), Type.Null()]),
195
- type: Type.String(),
196
- })
197
- ),
198
- nextPosition: Type.Union([Type.Number(), Type.Null()]),
199
- recentRuns: Type.Optional(Type.Array(Type.Object({ status: Type.String() }))),
200
- thread: Type.Object({ status: Type.String() }),
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 Value.Check(ThreadReadSchema, read) ? read : undefined;
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 StatusSchema = Type.Object({
362
- childThreadId: Type.String(),
363
- status: Type.Optional(Type.String()),
364
- workState: Type.Optional(Type.String()),
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 DelegateInputSchema = Type.Object({
368
- target: Type.Optional(Type.Object({ model: Type.String() })),
369
- });
381
+ const isDelegateInput = compile(
382
+ obj({ target: opt(obj({ model: str() }, { open: true })) }, { open: true })
383
+ ).check;
370
384
 
371
- const DelegateResultSchema = Type.Object({ childThreadId: Type.String() });
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 AssistantErrorSchema = Type.Object({
377
- errorMessage: Type.Optional(Type.String()),
378
- model: Type.String(),
379
- role: Type.Literal("assistant"),
380
- stopReason: Type.String(),
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(StatusSchema, event);
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(DelegateResultSchema, event);
459
+ const result = t3Json(delegateResult, event);
441
460
 
442
- const input = Value.Check(DelegateInputSchema, event.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 (!Value.Check(AssistantErrorSchema, last)) {
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 { Type } from "typebox";
8
- import type { Static } from "typebox";
9
- import { Value } from "typebox/value";
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 = Type.Object(
17
- {
18
- description: Type.Optional(
19
- Type.String({
20
- description: "Why to pick it, shown under the question.",
21
- minLength: 1,
22
- })
23
- ),
24
- label: Type.String({ description: "The choice.", minLength: 1 }),
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 AnswerSchema = Type.Union([
65
- Type.Object(
66
- {
67
- index: Type.Integer(),
68
- kind: Type.Literal("chose"),
69
- label: Type.String(),
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
- Type.Object(
75
- {
76
- kind: Type.Literal("typed"),
77
- question: Type.Integer(),
78
- text: Type.String(),
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
- Type.Object(
83
- { kind: Type.Literal("rephrase"), question: Type.Integer() },
84
- { additionalProperties: false }
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 = Type.Object(
89
- {
90
- kind: Type.Union([Type.Literal("dismissed"), Type.Literal("timed_out")]),
91
- question: Type.Integer(),
92
- unasked: Type.Integer(),
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 = Type.Object(
98
- {
99
- answers: Type.Array(AnswerSchema),
100
- stop: Type.Optional(StopSchema),
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 = Type.Object({});
94
+ const RawChoiceSchema = obj({}, { open: true });
119
95
 
120
- const RawQuestionSchema = Type.Object({
121
- choices: Type.Optional(Type.Array(RawChoiceSchema)),
122
- });
96
+ const RawQuestionSchema = obj(
97
+ { choices: opt(arr(RawChoiceSchema)) },
98
+ { open: true }
99
+ );
123
100
 
124
- const RawAskSchema = Type.Object({
125
- questions: Type.Optional(Type.Array(RawQuestionSchema)),
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 (Value.Check(RawAskSchema, args)) {
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 { Type } from "typebox";
3
- import { Value } from "typebox/value";
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 BashInputSchema = Type.Object({
17
- command: Type.String(),
18
- timeout: Type.Optional(Type.Unknown()),
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" || !Value.Check(BashInputSchema, input)) {
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
- Value.Check(Type.Number(), input.timeout) && input.timeout > 0
41
+ isNumber(input.timeout) && input.timeout > 0
35
42
  ? Math.min(input.timeout, cap)
36
43
  : cap;
37
44
  });
@@ -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 { Type } from "typebox";
7
- import type { Static, TSchema } from "typebox";
8
- import { Value } from "typebox/value";
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 = Type.Union([
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 = Type.Union([
21
- Type.Literal("on"),
22
- Type.Literal("off"),
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 (!Value.Check(Type.Record(Type.String(), Type.Unknown()), parsed)) {
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 = <S extends TSchema>(
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 || Value.Check(schema, value)) {
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
- Type.Number({ exclusiveMinimum: 0 })
91
- ),
92
- childSandbox: field("childSandbox", ChildSandboxSchema),
93
- classifierModel: field(
94
- "classifierModel",
95
- Type.String({ pattern: "^[^/\\s]+/.+$" })
96
+ fields.bashTimeoutSeconds
96
97
  ),
97
- mcpUrl: field("mcpUrl", Type.String({ minLength: 1 })),
98
- steer: field("steer", SteerSchema),
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
  };
@@ -3,7 +3,8 @@ import type {
3
3
  ExtensionAPI,
4
4
  ExtensionContext,
5
5
  } from "@earendil-works/pi-coding-agent";
6
- import { Type } from "typebox";
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 = Type.Object({
121
- plan: Type.String({
122
- description: "The plan in Markdown, as numbered steps.",
123
- minLength: 1,
124
- }),
125
- title: Type.Optional(
126
- Type.String({ description: "Short plan title.", minLength: 1 })
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 (toolCallId, params, _signal, _onUpdate, ctx) => {
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."