pi-learning-agent 0.1.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 (187) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/CONTRIBUTING.md +34 -0
  3. package/LICENSE +21 -0
  4. package/README.md +154 -0
  5. package/SECURITY.md +9 -0
  6. package/docs/architecture.md +75 -0
  7. package/docs/images/learning-workspace.png +0 -0
  8. package/docs/releasing.md +51 -0
  9. package/extension/commands.ts +181 -0
  10. package/extension/index.ts +50 -0
  11. package/extension/learning-session.ts +75 -0
  12. package/extension/runner/code-runner.ts +275 -0
  13. package/extension/server/interaction-broker.ts +345 -0
  14. package/extension/server/learning-server.ts +753 -0
  15. package/extension/server/sse-hub.ts +63 -0
  16. package/extension/state/learning-state.ts +458 -0
  17. package/extension/state/session-persistence.ts +72 -0
  18. package/extension/tools/ask-code.ts +97 -0
  19. package/extension/tools/ask-free-response.ts +88 -0
  20. package/extension/tools/ask-multi-choice.ts +109 -0
  21. package/extension/tools/ask-record-attempt.ts +50 -0
  22. package/extension/tools/ask-single-choice.ts +110 -0
  23. package/extension/tools/index.ts +39 -0
  24. package/extension/tools/tui-presenter.ts +609 -0
  25. package/extension/transcript-sync.ts +197 -0
  26. package/extension/tutor-prompt.ts +64 -0
  27. package/extension/utils/browser.ts +35 -0
  28. package/extension/utils/ids.ts +5 -0
  29. package/extension/utils/validation.ts +138 -0
  30. package/package.json +82 -0
  31. package/shared/learning-state.ts +62 -0
  32. package/shared/protocol.ts +236 -0
  33. package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  34. package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  35. package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  36. package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  37. package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  38. package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  39. package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  40. package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  41. package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  42. package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  43. package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  44. package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  45. package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  46. package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  47. package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  48. package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  49. package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  50. package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  51. package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  52. package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  53. package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  54. package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  55. package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  56. package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  57. package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  58. package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  59. package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  60. package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  61. package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  62. package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  63. package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  64. package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  65. package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  66. package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  67. package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  68. package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  69. package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  70. package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  71. package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  72. package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  73. package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  74. package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  75. package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  76. package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  77. package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  78. package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  79. package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  80. package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  81. package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  82. package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  83. package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  84. package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  85. package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  86. package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  87. package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  88. package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  89. package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  90. package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  91. package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  92. package/web/dist/assets/abap-08VXUWAP.js +1 -0
  93. package/web/dist/assets/apex-BWPQTe0t.js +1 -0
  94. package/web/dist/assets/azcli-Bc_sGQ0U.js +1 -0
  95. package/web/dist/assets/bat-i0X4ZdIN.js +1 -0
  96. package/web/dist/assets/bicep-B5-_aFwp.js +2 -0
  97. package/web/dist/assets/cameligo-DMUM7wLl.js +1 -0
  98. package/web/dist/assets/clojure-Cm7r79vr.js +1 -0
  99. package/web/dist/assets/codicon-Brq4_Ui5.ttf +0 -0
  100. package/web/dist/assets/coffee-Ba7i2nA0.js +1 -0
  101. package/web/dist/assets/cpp-C7h46wYY.js +1 -0
  102. package/web/dist/assets/csharp-BKxtCVv1.js +1 -0
  103. package/web/dist/assets/csp-bTuwJoIa.js +1 -0
  104. package/web/dist/assets/css-DIMkf-bt.js +3 -0
  105. package/web/dist/assets/css.worker-DRH2OcAG.js +93 -0
  106. package/web/dist/assets/cssMode-BpvqYhpN.js +1 -0
  107. package/web/dist/assets/cypher-CVaqCwHa.js +1 -0
  108. package/web/dist/assets/dart-onAF5SnQ.js +1 -0
  109. package/web/dist/assets/dockerfile-DZFCIeNp.js +1 -0
  110. package/web/dist/assets/ecl-D05T4iGw.js +1 -0
  111. package/web/dist/assets/editor.worker-D9zwrD0f.js +30 -0
  112. package/web/dist/assets/elixir-6RTg0lbw.js +1 -0
  113. package/web/dist/assets/flow9-C5_-GSwl.js +1 -0
  114. package/web/dist/assets/freemarker2-CmTivYCM.js +3 -0
  115. package/web/dist/assets/fsharp-C8Ef5oNN.js +1 -0
  116. package/web/dist/assets/go-C-y9NEjX.js +1 -0
  117. package/web/dist/assets/graphql-fmXr3nnJ.js +1 -0
  118. package/web/dist/assets/handlebars-ClhpojlY.js +1 -0
  119. package/web/dist/assets/hcl-CpzslTdj.js +1 -0
  120. package/web/dist/assets/html-CTqIxFH7.js +1 -0
  121. package/web/dist/assets/html.worker-DQLddT9h.js +506 -0
  122. package/web/dist/assets/htmlMode-BKCTQ4_a.js +1 -0
  123. package/web/dist/assets/index-6N_lnfyE.js +1243 -0
  124. package/web/dist/assets/index-B5x0MzHm.css +1 -0
  125. package/web/dist/assets/ini-sBoK_t0W.js +1 -0
  126. package/web/dist/assets/java-BEtHBSE6.js +1 -0
  127. package/web/dist/assets/javascript-Cp_q6aWP.js +1 -0
  128. package/web/dist/assets/json.worker-D_Dqrls5.js +62 -0
  129. package/web/dist/assets/jsonMode-CMDRrSMX.js +7 -0
  130. package/web/dist/assets/julia-Bri6UV-V.js +1 -0
  131. package/web/dist/assets/kotlin-BOotOW0E.js +1 -0
  132. package/web/dist/assets/less-B9JPFI3C.js +2 -0
  133. package/web/dist/assets/lexon-CfSJPG6W.js +1 -0
  134. package/web/dist/assets/liquid-BFK8j46k.js +1 -0
  135. package/web/dist/assets/lspLanguageFeatures-nT2b0EDJ.js +4 -0
  136. package/web/dist/assets/lua-CsQS60Ue.js +1 -0
  137. package/web/dist/assets/m3-D-oSqn_W.js +1 -0
  138. package/web/dist/assets/markdown-Cimd5fb3.js +1 -0
  139. package/web/dist/assets/mdx-cEJtnt-p.js +1 -0
  140. package/web/dist/assets/mips-CIPQ_RoX.js +1 -0
  141. package/web/dist/assets/msdax-DauUninz.js +1 -0
  142. package/web/dist/assets/mysql-SOo6toE5.js +1 -0
  143. package/web/dist/assets/objective-c-FvmIjYaQ.js +1 -0
  144. package/web/dist/assets/pascal-DrH0SRf2.js +1 -0
  145. package/web/dist/assets/pascaligo-D-ptJ9y-.js +1 -0
  146. package/web/dist/assets/perl-oz_6vUea.js +1 -0
  147. package/web/dist/assets/pgsql-DTj74zXo.js +1 -0
  148. package/web/dist/assets/php-nr791fC2.js +1 -0
  149. package/web/dist/assets/pla-CopQ2nXW.js +1 -0
  150. package/web/dist/assets/postiats-43DmfD33.js +1 -0
  151. package/web/dist/assets/powerquery-D3hlyOfw.js +1 -0
  152. package/web/dist/assets/powershell-DmHpPYUd.js +1 -0
  153. package/web/dist/assets/protobuf-C531GsRP.js +2 -0
  154. package/web/dist/assets/pug-Z5eAx3Zn.js +1 -0
  155. package/web/dist/assets/python-bhI_n2_n.js +1 -0
  156. package/web/dist/assets/qsharp-DkqhCAOL.js +1 -0
  157. package/web/dist/assets/r-BwWrilGY.js +1 -0
  158. package/web/dist/assets/razor-D40-I8aA.js +1 -0
  159. package/web/dist/assets/redis-ClamHrr6.js +1 -0
  160. package/web/dist/assets/redshift-DT7zqm-g.js +1 -0
  161. package/web/dist/assets/restructuredtext-BYgofb2h.js +1 -0
  162. package/web/dist/assets/ruby-DezsRK8O.js +1 -0
  163. package/web/dist/assets/rust-DdL9SqIa.js +1 -0
  164. package/web/dist/assets/sb-CcwsVR0C.js +1 -0
  165. package/web/dist/assets/scala-DHpiXF5c.js +1 -0
  166. package/web/dist/assets/scheme-BeGwcela.js +1 -0
  167. package/web/dist/assets/scss-gp-XZpBa.js +3 -0
  168. package/web/dist/assets/shell-CC2rA5mh.js +1 -0
  169. package/web/dist/assets/solidity-BEEn4gHE.js +1 -0
  170. package/web/dist/assets/sophia-CRfGWb83.js +1 -0
  171. package/web/dist/assets/sparql-D_Lu-MrJ.js +1 -0
  172. package/web/dist/assets/sql-NEE52Syq.js +1 -0
  173. package/web/dist/assets/st-DbInun42.js +1 -0
  174. package/web/dist/assets/swift-Bxkupp3x.js +1 -0
  175. package/web/dist/assets/systemverilog-Bz4Y3fRF.js +1 -0
  176. package/web/dist/assets/tcl-DISqw1ZD.js +1 -0
  177. package/web/dist/assets/ts.worker-DkYoA6bF.js +67738 -0
  178. package/web/dist/assets/tsMode-C9OJ2Aji.js +11 -0
  179. package/web/dist/assets/twig-De2hgUGE.js +1 -0
  180. package/web/dist/assets/typescript-CZ5RbSdB.js +1 -0
  181. package/web/dist/assets/typespec-B8J7ngcE.js +1 -0
  182. package/web/dist/assets/vb-DV3o63ZY.js +1 -0
  183. package/web/dist/assets/wgsl-DpFanUEy.js +298 -0
  184. package/web/dist/assets/workers-qb_EZhxe.js +1 -0
  185. package/web/dist/assets/xml-BdDnAXLl.js +1 -0
  186. package/web/dist/assets/yaml-BSYHoVQy.js +1 -0
  187. package/web/dist/index.html +23 -0
@@ -0,0 +1,88 @@
1
+ import {
2
+ defineTool,
3
+ type ExtensionContext
4
+ } from "@earendil-works/pi-coding-agent";
5
+ import { Type } from "typebox";
6
+
7
+ import type {
8
+ FreeResponseInteraction,
9
+ FreeResponseResolvedAnswer
10
+ } from "../../shared/protocol.js";
11
+ import { createInteractionId } from "../utils/ids.js";
12
+ import { presentFreeResponseInTui } from "./tui-presenter.js";
13
+
14
+ const parameters = Type.Object({
15
+ question: Type.String({ minLength: 1 }),
16
+ placeholder: Type.Optional(Type.String()),
17
+ multiline: Type.Optional(Type.Boolean()),
18
+ conceptId: Type.Optional(Type.String({ minLength: 1 }))
19
+ });
20
+
21
+ export interface FreeResponseToolDetails extends FreeResponseResolvedAnswer {
22
+ conceptId?: string;
23
+ }
24
+
25
+ export type FreeResponsePresenter = (
26
+ interaction: FreeResponseInteraction,
27
+ signal: AbortSignal | undefined,
28
+ ctx: ExtensionContext
29
+ ) => Promise<FreeResponseResolvedAnswer>;
30
+
31
+ export interface FreeResponseToolDependencies {
32
+ createId?: () => string;
33
+ now?: () => number;
34
+ present?: FreeResponsePresenter;
35
+ }
36
+
37
+ export function createAskFreeResponseTool(
38
+ dependencies: FreeResponseToolDependencies = {}
39
+ ) {
40
+ const createId = dependencies.createId ?? (() => createInteractionId("free"));
41
+ const now = dependencies.now ?? Date.now;
42
+ const present =
43
+ dependencies.present ??
44
+ ((interaction, signal, ctx) =>
45
+ presentFreeResponseInTui(interaction, signal, ctx, now));
46
+
47
+ return defineTool<typeof parameters, FreeResponseToolDetails>({
48
+ name: "learning_ask_free_response",
49
+ label: "Learning: Free Response",
50
+ description:
51
+ "Ask the learner an open-ended question and wait for their structured response.",
52
+ promptSnippet: "Ask a structured open-ended learning question",
53
+ executionMode: "sequential",
54
+ parameters,
55
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
56
+ const interaction: FreeResponseInteraction = {
57
+ id: createId(),
58
+ type: "free_response",
59
+ question: params.question,
60
+ multiline: params.multiline ?? true,
61
+ createdAt: now(),
62
+ ...(params.placeholder === undefined
63
+ ? {}
64
+ : { placeholder: params.placeholder }),
65
+ ...(params.conceptId === undefined
66
+ ? {}
67
+ : { conceptId: params.conceptId })
68
+ };
69
+ const resolved = await present(interaction, signal, ctx);
70
+ const details: FreeResponseToolDetails = {
71
+ ...resolved,
72
+ ...(interaction.conceptId === undefined
73
+ ? {}
74
+ : { conceptId: interaction.conceptId })
75
+ };
76
+
77
+ return {
78
+ content: [
79
+ {
80
+ type: "text",
81
+ text: `Learner submitted this free response:\n${resolved.answer.text}\n(interaction ${resolved.interactionId})`
82
+ }
83
+ ],
84
+ details
85
+ };
86
+ }
87
+ });
88
+ }
@@ -0,0 +1,109 @@
1
+ import {
2
+ defineTool,
3
+ type ExtensionContext
4
+ } from "@earendil-works/pi-coding-agent";
5
+ import { Type } from "typebox";
6
+
7
+ import type {
8
+ MultiChoiceAnswer,
9
+ MultiChoiceInteraction
10
+ } from "../../shared/protocol.js";
11
+ import { createInteractionId } from "../utils/ids.js";
12
+ import { presentMultiChoiceInTui } from "./tui-presenter.js";
13
+ import type { MultiChoiceResolvedAnswerOrSkip } from "./tui-presenter.js";
14
+
15
+ const parameters = Type.Object({
16
+ title: Type.Optional(Type.String({ minLength: 1 })),
17
+ question: Type.String({ minLength: 1 }),
18
+ options: Type.Array(
19
+ Type.Object({
20
+ id: Type.String({ minLength: 1 }),
21
+ label: Type.String({ minLength: 1 })
22
+ }),
23
+ { minItems: 2 }
24
+ ),
25
+ conceptId: Type.Optional(Type.String({ minLength: 1 })),
26
+ allowSkip: Type.Optional(Type.Boolean())
27
+ });
28
+
29
+ /**
30
+ * Answered or skipped (spec 7.5): a skipped interaction carries
31
+ * `skipped: true` and no answer.
32
+ */
33
+ export type MultiChoiceToolDetails = (
34
+ | { interactionId: string; type: "multi_choice"; answer: MultiChoiceAnswer; responseTimeMs: number }
35
+ | { interactionId: string; type: "multi_choice"; skipped: true; responseTimeMs: number }
36
+ ) & { conceptId?: string };
37
+
38
+ export interface MultiChoiceToolDependencies {
39
+ createId?: () => string;
40
+ now?: () => number;
41
+ present?: MultiChoicePresenter;
42
+ }
43
+
44
+ export type MultiChoicePresenter = (
45
+ interaction: MultiChoiceInteraction,
46
+ signal: AbortSignal | undefined,
47
+ ctx: ExtensionContext
48
+ ) => Promise<MultiChoiceResolvedAnswerOrSkip>;
49
+
50
+ export function createAskMultiChoiceTool(
51
+ dependencies: MultiChoiceToolDependencies = {}
52
+ ) {
53
+ const createId =
54
+ dependencies.createId ?? (() => createInteractionId("multi"));
55
+ const now = dependencies.now ?? Date.now;
56
+ const present =
57
+ dependencies.present ??
58
+ ((interaction, signal, ctx) =>
59
+ presentMultiChoiceInTui(interaction, signal, ctx, now));
60
+
61
+ return defineTool<typeof parameters, MultiChoiceToolDetails>({
62
+ name: "learning_ask_multi_choice",
63
+ label: "Learning: Multi Choice",
64
+ description:
65
+ "Ask the learner a multi-choice question (one or more answers may be correct) and wait for their structured answer. allowSkip lets the learner decline without answering.",
66
+ promptSnippet: "Ask a structured multi-choice learning question",
67
+ executionMode: "sequential",
68
+ parameters,
69
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
70
+ const interaction: MultiChoiceInteraction = {
71
+ id: createId(),
72
+ type: "multi_choice",
73
+ question: params.question,
74
+ options: params.options,
75
+ allowSkip: params.allowSkip ?? false,
76
+ createdAt: now(),
77
+ ...(params.title === undefined ? {} : { title: params.title }),
78
+ ...(params.conceptId === undefined
79
+ ? {}
80
+ : { conceptId: params.conceptId })
81
+ };
82
+ const resolved = await present(interaction, signal, ctx);
83
+ const skipped = "skipped" in resolved;
84
+ const details: MultiChoiceToolDetails = {
85
+ interactionId: resolved.interactionId,
86
+ type: "multi_choice",
87
+ ...(skipped
88
+ ? { skipped: true }
89
+ : { answer: resolved.answer }),
90
+ responseTimeMs: resolved.responseTimeMs,
91
+ ...(interaction.conceptId === undefined
92
+ ? {}
93
+ : { conceptId: interaction.conceptId })
94
+ };
95
+
96
+ return {
97
+ content: [
98
+ {
99
+ type: "text",
100
+ text: skipped
101
+ ? `Learner skipped the question (interaction ${resolved.interactionId}).`
102
+ : `Learner selected options ${resolved.answer.optionIds.join(", ")} (interaction ${resolved.interactionId}).`
103
+ }
104
+ ],
105
+ details
106
+ };
107
+ }
108
+ });
109
+ }
@@ -0,0 +1,50 @@
1
+ import { defineTool } from "@earendil-works/pi-coding-agent";
2
+ import { StringEnum } from "@earendil-works/pi-ai";
3
+ import { Type } from "typebox";
4
+ import type { LearningSession, AttemptEvaluation } from "../learning-session.js";
5
+
6
+ const parameters = Type.Object({
7
+ interactionId: Type.String({ minLength: 1 }),
8
+ conceptId: Type.String({ minLength: 1 }),
9
+ outcome: StringEnum(["correct", "partial", "incorrect"] as const),
10
+ evidenceType: StringEnum(["choice", "free_response", "code"] as const),
11
+ explanation: Type.String({
12
+ minLength: 1,
13
+ pattern: "\\S",
14
+ description: "Learner-facing explanation of the correct answer and key reasoning, in the learner's language. Shown with the outcome before the next question."
15
+ }),
16
+ misconception: Type.Optional(Type.String({ minLength: 1 }))
17
+ });
18
+
19
+ export interface RecordAttemptToolDetails extends AttemptEvaluation {
20
+ explanation: string;
21
+ }
22
+
23
+ /** Pi adapter:只负责参数与结果文案,评分规则属于 LearningSession。 */
24
+ export function createAskRecordAttemptTool(session: Pick<LearningSession, "recordAttempt">) {
25
+ return defineTool<typeof parameters, RecordAttemptToolDetails>({
26
+ name: "learning_record_attempt",
27
+ label: "Learning: Record Attempt",
28
+ description:
29
+ "Record the learner's attempt on a concept after evaluating a learning_ask_* answer. Always include an explanation of the correct answer and key reasoning in the learner's language; the workspace displays it with the outcome. Complete this feedback before asking the next question. Do NOT record attempts for interactions whose tool result says skipped: true — a skipped question is not an answer.",
30
+ promptSnippet: "Record the learner's attempt with an explanation and update concept mastery",
31
+ parameters,
32
+ async execute(_toolCallId, params) {
33
+ const details: RecordAttemptToolDetails = {
34
+ ...session.recordAttempt(params),
35
+ explanation: params.explanation
36
+ };
37
+ const { previousMastery, newMastery } = details;
38
+
39
+ return {
40
+ content: [
41
+ {
42
+ type: "text",
43
+ text: `Recorded attempt for concept ${params.conceptId}: ${params.outcome} (${previousMastery.toFixed(2)} → ${newMastery.toFixed(2)}).`
44
+ }
45
+ ],
46
+ details
47
+ };
48
+ }
49
+ });
50
+ }
@@ -0,0 +1,110 @@
1
+ import {
2
+ defineTool,
3
+ type ExtensionContext
4
+ } from "@earendil-works/pi-coding-agent";
5
+ import { Type } from "typebox";
6
+
7
+ import type {
8
+ SingleChoiceAnswer,
9
+ SingleChoiceInteraction
10
+ } from "../../shared/protocol.js";
11
+ import { createInteractionId } from "../utils/ids.js";
12
+ import { presentSingleChoiceInTui } from "./tui-presenter.js";
13
+ import type { ChoiceResolvedAnswer } from "./tui-presenter.js";
14
+
15
+ const parameters = Type.Object({
16
+ title: Type.Optional(Type.String({ minLength: 1 })),
17
+ question: Type.String({ minLength: 1 }),
18
+ options: Type.Array(
19
+ Type.Object({
20
+ id: Type.String({ minLength: 1 }),
21
+ label: Type.String({ minLength: 1 })
22
+ }),
23
+ { minItems: 2 }
24
+ ),
25
+ conceptId: Type.Optional(Type.String({ minLength: 1 })),
26
+ allowSkip: Type.Optional(Type.Boolean())
27
+ });
28
+
29
+ /**
30
+ * Answered or skipped (spec 7.5): a skipped interaction carries
31
+ * `skipped: true` and no answer, so the model can tell a decline from a
32
+ * wrong answer.
33
+ */
34
+ export type SingleChoiceToolDetails = (
35
+ | { interactionId: string; type: "single_choice"; answer: SingleChoiceAnswer; responseTimeMs: number }
36
+ | { interactionId: string; type: "single_choice"; skipped: true; responseTimeMs: number }
37
+ ) & { conceptId?: string };
38
+
39
+ export interface SingleChoiceToolDependencies {
40
+ createId?: () => string;
41
+ now?: () => number;
42
+ present?: SingleChoicePresenter;
43
+ }
44
+
45
+ export type SingleChoicePresenter = (
46
+ interaction: SingleChoiceInteraction,
47
+ signal: AbortSignal | undefined,
48
+ ctx: ExtensionContext
49
+ ) => Promise<ChoiceResolvedAnswer>;
50
+
51
+ export function createAskSingleChoiceTool(
52
+ dependencies: SingleChoiceToolDependencies = {}
53
+ ) {
54
+ const createId =
55
+ dependencies.createId ?? (() => createInteractionId("choice"));
56
+ const now = dependencies.now ?? Date.now;
57
+ const present =
58
+ dependencies.present ??
59
+ ((interaction, signal, ctx) =>
60
+ presentSingleChoiceInTui(interaction, signal, ctx, now));
61
+
62
+ return defineTool<typeof parameters, SingleChoiceToolDetails>({
63
+ name: "learning_ask_single_choice",
64
+ label: "Learning: Single Choice",
65
+ description:
66
+ "Ask the learner one single-choice question and wait for their structured answer. allowSkip lets the learner decline without answering.",
67
+ promptSnippet: "Ask a structured single-choice learning question",
68
+ executionMode: "sequential",
69
+ parameters,
70
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
71
+ const interaction: SingleChoiceInteraction = {
72
+ id: createId(),
73
+ type: "single_choice",
74
+ question: params.question,
75
+ options: params.options,
76
+ allowSkip: params.allowSkip ?? false,
77
+ createdAt: now(),
78
+ ...(params.title === undefined ? {} : { title: params.title }),
79
+ ...(params.conceptId === undefined
80
+ ? {}
81
+ : { conceptId: params.conceptId })
82
+ };
83
+ const resolved = await present(interaction, signal, ctx);
84
+ const skipped = "skipped" in resolved;
85
+ const details: SingleChoiceToolDetails = {
86
+ interactionId: resolved.interactionId,
87
+ type: "single_choice",
88
+ ...(skipped
89
+ ? { skipped: true }
90
+ : { answer: resolved.answer }),
91
+ responseTimeMs: resolved.responseTimeMs,
92
+ ...(interaction.conceptId === undefined
93
+ ? {}
94
+ : { conceptId: interaction.conceptId })
95
+ };
96
+
97
+ return {
98
+ content: [
99
+ {
100
+ type: "text",
101
+ text: skipped
102
+ ? `Learner skipped the question (interaction ${resolved.interactionId}).`
103
+ : `Learner selected option ${resolved.answer.optionId} (interaction ${resolved.interactionId}).`
104
+ }
105
+ ],
106
+ details
107
+ };
108
+ }
109
+ });
110
+ }
@@ -0,0 +1,39 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ import type { LearningSession } from "../learning-session.js";
4
+ import type { LearningServer } from "../server/learning-server.js";
5
+ import { createAskCodeTool } from "./ask-code.js";
6
+ import { createAskFreeResponseTool } from "./ask-free-response.js";
7
+ import { createAskMultiChoiceTool } from "./ask-multi-choice.js";
8
+ import { createAskRecordAttemptTool } from "./ask-record-attempt.js";
9
+ import { createAskSingleChoiceTool } from "./ask-single-choice.js";
10
+ import { createModeAwarePresenter } from "./tui-presenter.js";
11
+
12
+ export interface LearningToolRegistrationDependencies {
13
+ session: LearningSession;
14
+ /** Source of hasWebClient() for uiMode auto routing (spec 28). */
15
+ server: LearningServer;
16
+ }
17
+
18
+ export function registerLearningTools(
19
+ pi: ExtensionAPI,
20
+ dependencies: LearningToolRegistrationDependencies
21
+ ): void {
22
+ const presenter = createModeAwarePresenter(
23
+ dependencies.session.broker,
24
+ () => dependencies.server.hasWebClient()
25
+ );
26
+ pi.registerTool(
27
+ createAskSingleChoiceTool({ present: presenter.presentSingleChoice })
28
+ );
29
+ pi.registerTool(
30
+ createAskMultiChoiceTool({ present: presenter.presentMultiChoice })
31
+ );
32
+ pi.registerTool(
33
+ createAskFreeResponseTool({ present: presenter.presentFreeResponse })
34
+ );
35
+ pi.registerTool(createAskCodeTool({ present: presenter.presentCode }));
36
+ pi.registerTool(
37
+ createAskRecordAttemptTool(dependencies.session)
38
+ );
39
+ }