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.
- package/CHANGELOG.md +19 -0
- package/CONTRIBUTING.md +34 -0
- package/LICENSE +21 -0
- package/README.md +154 -0
- package/SECURITY.md +9 -0
- package/docs/architecture.md +75 -0
- package/docs/images/learning-workspace.png +0 -0
- package/docs/releasing.md +51 -0
- package/extension/commands.ts +181 -0
- package/extension/index.ts +50 -0
- package/extension/learning-session.ts +75 -0
- package/extension/runner/code-runner.ts +275 -0
- package/extension/server/interaction-broker.ts +345 -0
- package/extension/server/learning-server.ts +753 -0
- package/extension/server/sse-hub.ts +63 -0
- package/extension/state/learning-state.ts +458 -0
- package/extension/state/session-persistence.ts +72 -0
- package/extension/tools/ask-code.ts +97 -0
- package/extension/tools/ask-free-response.ts +88 -0
- package/extension/tools/ask-multi-choice.ts +109 -0
- package/extension/tools/ask-record-attempt.ts +50 -0
- package/extension/tools/ask-single-choice.ts +110 -0
- package/extension/tools/index.ts +39 -0
- package/extension/tools/tui-presenter.ts +609 -0
- package/extension/transcript-sync.ts +197 -0
- package/extension/tutor-prompt.ts +64 -0
- package/extension/utils/browser.ts +35 -0
- package/extension/utils/ids.ts +5 -0
- package/extension/utils/validation.ts +138 -0
- package/package.json +82 -0
- package/shared/learning-state.ts +62 -0
- package/shared/protocol.ts +236 -0
- package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/web/dist/assets/abap-08VXUWAP.js +1 -0
- package/web/dist/assets/apex-BWPQTe0t.js +1 -0
- package/web/dist/assets/azcli-Bc_sGQ0U.js +1 -0
- package/web/dist/assets/bat-i0X4ZdIN.js +1 -0
- package/web/dist/assets/bicep-B5-_aFwp.js +2 -0
- package/web/dist/assets/cameligo-DMUM7wLl.js +1 -0
- package/web/dist/assets/clojure-Cm7r79vr.js +1 -0
- package/web/dist/assets/codicon-Brq4_Ui5.ttf +0 -0
- package/web/dist/assets/coffee-Ba7i2nA0.js +1 -0
- package/web/dist/assets/cpp-C7h46wYY.js +1 -0
- package/web/dist/assets/csharp-BKxtCVv1.js +1 -0
- package/web/dist/assets/csp-bTuwJoIa.js +1 -0
- package/web/dist/assets/css-DIMkf-bt.js +3 -0
- package/web/dist/assets/css.worker-DRH2OcAG.js +93 -0
- package/web/dist/assets/cssMode-BpvqYhpN.js +1 -0
- package/web/dist/assets/cypher-CVaqCwHa.js +1 -0
- package/web/dist/assets/dart-onAF5SnQ.js +1 -0
- package/web/dist/assets/dockerfile-DZFCIeNp.js +1 -0
- package/web/dist/assets/ecl-D05T4iGw.js +1 -0
- package/web/dist/assets/editor.worker-D9zwrD0f.js +30 -0
- package/web/dist/assets/elixir-6RTg0lbw.js +1 -0
- package/web/dist/assets/flow9-C5_-GSwl.js +1 -0
- package/web/dist/assets/freemarker2-CmTivYCM.js +3 -0
- package/web/dist/assets/fsharp-C8Ef5oNN.js +1 -0
- package/web/dist/assets/go-C-y9NEjX.js +1 -0
- package/web/dist/assets/graphql-fmXr3nnJ.js +1 -0
- package/web/dist/assets/handlebars-ClhpojlY.js +1 -0
- package/web/dist/assets/hcl-CpzslTdj.js +1 -0
- package/web/dist/assets/html-CTqIxFH7.js +1 -0
- package/web/dist/assets/html.worker-DQLddT9h.js +506 -0
- package/web/dist/assets/htmlMode-BKCTQ4_a.js +1 -0
- package/web/dist/assets/index-6N_lnfyE.js +1243 -0
- package/web/dist/assets/index-B5x0MzHm.css +1 -0
- package/web/dist/assets/ini-sBoK_t0W.js +1 -0
- package/web/dist/assets/java-BEtHBSE6.js +1 -0
- package/web/dist/assets/javascript-Cp_q6aWP.js +1 -0
- package/web/dist/assets/json.worker-D_Dqrls5.js +62 -0
- package/web/dist/assets/jsonMode-CMDRrSMX.js +7 -0
- package/web/dist/assets/julia-Bri6UV-V.js +1 -0
- package/web/dist/assets/kotlin-BOotOW0E.js +1 -0
- package/web/dist/assets/less-B9JPFI3C.js +2 -0
- package/web/dist/assets/lexon-CfSJPG6W.js +1 -0
- package/web/dist/assets/liquid-BFK8j46k.js +1 -0
- package/web/dist/assets/lspLanguageFeatures-nT2b0EDJ.js +4 -0
- package/web/dist/assets/lua-CsQS60Ue.js +1 -0
- package/web/dist/assets/m3-D-oSqn_W.js +1 -0
- package/web/dist/assets/markdown-Cimd5fb3.js +1 -0
- package/web/dist/assets/mdx-cEJtnt-p.js +1 -0
- package/web/dist/assets/mips-CIPQ_RoX.js +1 -0
- package/web/dist/assets/msdax-DauUninz.js +1 -0
- package/web/dist/assets/mysql-SOo6toE5.js +1 -0
- package/web/dist/assets/objective-c-FvmIjYaQ.js +1 -0
- package/web/dist/assets/pascal-DrH0SRf2.js +1 -0
- package/web/dist/assets/pascaligo-D-ptJ9y-.js +1 -0
- package/web/dist/assets/perl-oz_6vUea.js +1 -0
- package/web/dist/assets/pgsql-DTj74zXo.js +1 -0
- package/web/dist/assets/php-nr791fC2.js +1 -0
- package/web/dist/assets/pla-CopQ2nXW.js +1 -0
- package/web/dist/assets/postiats-43DmfD33.js +1 -0
- package/web/dist/assets/powerquery-D3hlyOfw.js +1 -0
- package/web/dist/assets/powershell-DmHpPYUd.js +1 -0
- package/web/dist/assets/protobuf-C531GsRP.js +2 -0
- package/web/dist/assets/pug-Z5eAx3Zn.js +1 -0
- package/web/dist/assets/python-bhI_n2_n.js +1 -0
- package/web/dist/assets/qsharp-DkqhCAOL.js +1 -0
- package/web/dist/assets/r-BwWrilGY.js +1 -0
- package/web/dist/assets/razor-D40-I8aA.js +1 -0
- package/web/dist/assets/redis-ClamHrr6.js +1 -0
- package/web/dist/assets/redshift-DT7zqm-g.js +1 -0
- package/web/dist/assets/restructuredtext-BYgofb2h.js +1 -0
- package/web/dist/assets/ruby-DezsRK8O.js +1 -0
- package/web/dist/assets/rust-DdL9SqIa.js +1 -0
- package/web/dist/assets/sb-CcwsVR0C.js +1 -0
- package/web/dist/assets/scala-DHpiXF5c.js +1 -0
- package/web/dist/assets/scheme-BeGwcela.js +1 -0
- package/web/dist/assets/scss-gp-XZpBa.js +3 -0
- package/web/dist/assets/shell-CC2rA5mh.js +1 -0
- package/web/dist/assets/solidity-BEEn4gHE.js +1 -0
- package/web/dist/assets/sophia-CRfGWb83.js +1 -0
- package/web/dist/assets/sparql-D_Lu-MrJ.js +1 -0
- package/web/dist/assets/sql-NEE52Syq.js +1 -0
- package/web/dist/assets/st-DbInun42.js +1 -0
- package/web/dist/assets/swift-Bxkupp3x.js +1 -0
- package/web/dist/assets/systemverilog-Bz4Y3fRF.js +1 -0
- package/web/dist/assets/tcl-DISqw1ZD.js +1 -0
- package/web/dist/assets/ts.worker-DkYoA6bF.js +67738 -0
- package/web/dist/assets/tsMode-C9OJ2Aji.js +11 -0
- package/web/dist/assets/twig-De2hgUGE.js +1 -0
- package/web/dist/assets/typescript-CZ5RbSdB.js +1 -0
- package/web/dist/assets/typespec-B8J7ngcE.js +1 -0
- package/web/dist/assets/vb-DV3o63ZY.js +1 -0
- package/web/dist/assets/wgsl-DpFanUEy.js +298 -0
- package/web/dist/assets/workers-qb_EZhxe.js +1 -0
- package/web/dist/assets/xml-BdDnAXLl.js +1 -0
- package/web/dist/assets/yaml-BSYHoVQy.js +1 -0
- 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
|
+
}
|