@rimori/client 2.5.55-next.0 → 2.5.56-next.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/dist/controller/AccomplishmentController.d.ts +10 -1
- package/dist/controller/AccomplishmentController.js +0 -6
- package/dist/fromRimori/ActionParams.d.ts +23 -0
- package/dist/fromRimori/ActionParams.js +28 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +3 -0
- package/dist/modules.d.ts +3 -0
- package/dist/modules.js +2 -0
- package/dist/plugin/RimoriClient.d.ts +8 -0
- package/dist/plugin/RimoriClient.js +10 -0
- package/dist/plugin/module/AIModule.d.ts +57 -0
- package/dist/plugin/module/AIModule.js +116 -12
- package/dist/plugin/module/ExerciseModule.d.ts +89 -40
- package/dist/plugin/module/ExerciseModule.js +65 -23
- package/dist/plugin/module/PluginModule.d.ts +8 -0
- package/package.json +1 -1
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { EventBusHandler, EventBusMessage } from '../fromRimori/EventBus';
|
|
2
2
|
export type AccomplishmentMessage = EventBusMessage<MicroAccomplishmentPayload>;
|
|
3
3
|
export declare const skillCategories: readonly ["reading", "listening", "speaking", "writing", "learning", "community"];
|
|
4
|
+
/**
|
|
5
|
+
* The single declaration of the accomplishment skill categories. Previously re-typed by hand in
|
|
6
|
+
* `useAccomplishmentChecker` (rimori-main) and `UseExerciseHook` (react-client), which could
|
|
7
|
+
* drift apart silently.
|
|
8
|
+
*
|
|
9
|
+
* Distinct from rimori-main's `SkillCategory` in `types/skill_progress.ts`: that one names the
|
|
10
|
+
* columns of the `skill_progress` table (…, 'understanding', 'grammar') and is a different set.
|
|
11
|
+
*/
|
|
12
|
+
export type SkillCategory = (typeof skillCategories)[number];
|
|
4
13
|
interface BaseAccomplishmentPayload {
|
|
5
14
|
type: 'micro' | 'macro';
|
|
6
|
-
skillCategory:
|
|
15
|
+
skillCategory: SkillCategory;
|
|
7
16
|
accomplishmentKeyword: string;
|
|
8
17
|
description: string;
|
|
9
18
|
meta?: {
|
|
@@ -35,12 +35,6 @@ export class AccomplishmentController {
|
|
|
35
35
|
if (!['micro', 'macro'].includes(payload.type)) {
|
|
36
36
|
throw new Error('Invalid accomplishment type ' + payload.type);
|
|
37
37
|
}
|
|
38
|
-
// disabled detection temporarelly to determine how long exercises normally are
|
|
39
|
-
//durationMinutes is required
|
|
40
|
-
// if (payload.type === 'macro' && payload.durationMinutes < 4) {
|
|
41
|
-
// console.warn('The duration must be at least 4 minutes');
|
|
42
|
-
// return false;
|
|
43
|
-
// }
|
|
44
38
|
//errorRatio is required
|
|
45
39
|
if (payload.type === 'macro' && (payload.errorRatio < 0 || payload.errorRatio > 1)) {
|
|
46
40
|
throw new Error('The error ratio must be between 0 and 1');
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one validator for "does this value set satisfy an action's declared parameter schema?".
|
|
3
|
+
*
|
|
4
|
+
* There used to be a copy of this rule per caller — the dashboard's CreateExerciseDialog gated
|
|
5
|
+
* its Next button on it, and RouterHandler threw on it before navigating — which let them
|
|
6
|
+
* disagree about what counts as a provided value. They now share this.
|
|
7
|
+
*
|
|
8
|
+
* Note this is a *pre-flight* check, not a trust boundary: it runs in the browser, so anything
|
|
9
|
+
* server-side must re-validate rather than trust a caller that says it already did.
|
|
10
|
+
*/
|
|
11
|
+
/** The subset of `ToolParameter` this check needs — kept structural so any superset fits. */
|
|
12
|
+
export interface ActionParameterSchema {
|
|
13
|
+
optional?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Names of the required (non-`optional`) parameters that `values` does not provide.
|
|
17
|
+
*
|
|
18
|
+
* "Not provided" is `undefined`, `null` or the empty string only — `false` and `0` are real
|
|
19
|
+
* answers for a boolean/number parameter and must not read as missing.
|
|
20
|
+
*/
|
|
21
|
+
export declare function findMissingActionParams(parameters: Record<string, ActionParameterSchema>, values: Record<string, unknown>): string[];
|
|
22
|
+
/** True when every required parameter of the action is provided. */
|
|
23
|
+
export declare function hasRequiredActionParams(parameters: Record<string, ActionParameterSchema>, values: Record<string, unknown>): boolean;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one validator for "does this value set satisfy an action's declared parameter schema?".
|
|
3
|
+
*
|
|
4
|
+
* There used to be a copy of this rule per caller — the dashboard's CreateExerciseDialog gated
|
|
5
|
+
* its Next button on it, and RouterHandler threw on it before navigating — which let them
|
|
6
|
+
* disagree about what counts as a provided value. They now share this.
|
|
7
|
+
*
|
|
8
|
+
* Note this is a *pre-flight* check, not a trust boundary: it runs in the browser, so anything
|
|
9
|
+
* server-side must re-validate rather than trust a caller that says it already did.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Names of the required (non-`optional`) parameters that `values` does not provide.
|
|
13
|
+
*
|
|
14
|
+
* "Not provided" is `undefined`, `null` or the empty string only — `false` and `0` are real
|
|
15
|
+
* answers for a boolean/number parameter and must not read as missing.
|
|
16
|
+
*/
|
|
17
|
+
export function findMissingActionParams(parameters, values) {
|
|
18
|
+
return Object.keys(parameters).filter((key) => {
|
|
19
|
+
if (parameters[key]?.optional)
|
|
20
|
+
return false;
|
|
21
|
+
const value = values[key];
|
|
22
|
+
return value === undefined || value === null || value === '';
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/** True when every required parameter of the action is provided. */
|
|
26
|
+
export function hasRequiredActionParams(parameters, values) {
|
|
27
|
+
return findMissingActionParams(parameters, values).length === 0;
|
|
28
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './fromRimori/EventBus';
|
|
2
2
|
export * from './plugin/RimoriClient';
|
|
3
3
|
export * from './fromRimori/PluginTypes';
|
|
4
|
+
export * from './fromRimori/ActionParams';
|
|
4
5
|
export * from './plugin/StandaloneClient';
|
|
5
6
|
export * from './cli/types/DatabaseTypes';
|
|
6
7
|
export * from './cli/types/PromptTypes';
|
|
@@ -12,15 +13,16 @@ export { setupWorker } from './worker/WorkerSetup';
|
|
|
12
13
|
export type { EventBusMessage } from './fromRimori/EventBus';
|
|
13
14
|
export { AudioController } from './controller/AudioController';
|
|
14
15
|
export { voiceDebug } from './plugin/VoiceDebugStore';
|
|
15
|
-
export type {
|
|
16
|
+
export type { ExerciseRow, CreateExerciseRowParams, CompleteExerciseParams, EvidenceKind } from './plugin/module/ExerciseModule';
|
|
16
17
|
export { Translator } from './controller/TranslationController';
|
|
17
|
-
export type {
|
|
18
|
-
export
|
|
18
|
+
export type { Message, ToolInvocation, WireMessage, ToolCallContentPart, ToolResultContentPart, ToolResultOutput } from './plugin/module/AIModule';
|
|
19
|
+
export { toModelMessages } from './plugin/module/AIModule';
|
|
19
20
|
export type { Theme, ApplicationMode } from './plugin/module/PluginModule';
|
|
20
21
|
export type { UserInfo, Language, UserRole, SubscriptionTier, ExplicitUndefined, BasePluginSettings, Buddy } from './plugin/module/PluginModule';
|
|
21
22
|
export { TIER_ORDER, ROLE_ORDER } from './plugin/module/PluginModule';
|
|
22
23
|
export type { SharedContent, BasicSharedContent, ContentStatus } from './plugin/module/SharedContentController';
|
|
23
|
-
export type { MacroAccomplishmentPayload, MicroAccomplishmentPayload } from './controller/AccomplishmentController';
|
|
24
|
+
export type { MacroAccomplishmentPayload, MicroAccomplishmentPayload, AccomplishmentPayload, SkillCategory, } from './controller/AccomplishmentController';
|
|
25
|
+
export { skillCategories } from './controller/AccomplishmentController';
|
|
24
26
|
export { StorageModule } from './plugin/module/StorageModule';
|
|
25
27
|
export { AssetsModule, type AssetKind } from './plugin/module/AssetsModule';
|
|
26
28
|
export type { PublicityLevel } from './plugin/module/DbModule';
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export * from './fromRimori/EventBus';
|
|
3
3
|
export * from './plugin/RimoriClient';
|
|
4
4
|
export * from './fromRimori/PluginTypes';
|
|
5
|
+
export * from './fromRimori/ActionParams';
|
|
5
6
|
export * from './plugin/StandaloneClient';
|
|
6
7
|
export * from './cli/types/DatabaseTypes';
|
|
7
8
|
export * from './cli/types/PromptTypes';
|
|
@@ -12,6 +13,8 @@ export { setupWorker } from './worker/WorkerSetup';
|
|
|
12
13
|
export { AudioController } from './controller/AudioController';
|
|
13
14
|
export { voiceDebug } from './plugin/VoiceDebugStore';
|
|
14
15
|
export { Translator } from './controller/TranslationController';
|
|
16
|
+
export { toModelMessages } from './plugin/module/AIModule';
|
|
15
17
|
export { TIER_ORDER, ROLE_ORDER } from './plugin/module/PluginModule';
|
|
18
|
+
export { skillCategories } from './controller/AccomplishmentController';
|
|
16
19
|
export { StorageModule } from './plugin/module/StorageModule';
|
|
17
20
|
export { AssetsModule } from './plugin/module/AssetsModule';
|
package/dist/modules.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export * from './fromRimori/EventBus';
|
|
2
2
|
export * from './plugin/module/AIModule';
|
|
3
3
|
export * from './fromRimori/PluginTypes';
|
|
4
|
+
export * from './fromRimori/ActionParams';
|
|
5
|
+
export type { MacroAccomplishmentPayload, MicroAccomplishmentPayload, AccomplishmentPayload, SkillCategory, } from './controller/AccomplishmentController';
|
|
6
|
+
export { skillCategories } from './controller/AccomplishmentController';
|
|
4
7
|
export * from './cli/types/DatabaseTypes';
|
|
5
8
|
export * from './plugin/TTS/MessageSender';
|
package/dist/modules.js
CHANGED
|
@@ -2,5 +2,7 @@
|
|
|
2
2
|
export * from './fromRimori/EventBus';
|
|
3
3
|
export * from './plugin/module/AIModule';
|
|
4
4
|
export * from './fromRimori/PluginTypes';
|
|
5
|
+
export * from './fromRimori/ActionParams';
|
|
6
|
+
export { skillCategories } from './controller/AccomplishmentController';
|
|
5
7
|
export * from './cli/types/DatabaseTypes';
|
|
6
8
|
export * from './plugin/TTS/MessageSender';
|
|
@@ -31,6 +31,14 @@ export declare class RimoriClient {
|
|
|
31
31
|
*/
|
|
32
32
|
static createWithInfo(info: RimoriInfo, applicationMode?: ApplicationMode): RimoriClient;
|
|
33
33
|
static getInstance(pluginId?: string): Promise<RimoriClient>;
|
|
34
|
+
/**
|
|
35
|
+
* Resolves once no plugin operation that still has to emit on the event bus is in flight.
|
|
36
|
+
*
|
|
37
|
+
* The host (PluginRenderer) awaits this before tearing the plugin's event bus down, so a
|
|
38
|
+
* fire-and-forget mutation started just before the user navigated away still gets its event
|
|
39
|
+
* delivered. Extend this if another module ever gains an emit that happens *after* an await.
|
|
40
|
+
*/
|
|
41
|
+
whenIdle(): Promise<void>;
|
|
34
42
|
navigation: {
|
|
35
43
|
toDashboard: () => void;
|
|
36
44
|
};
|
|
@@ -89,6 +89,16 @@ export class RimoriClient {
|
|
|
89
89
|
}
|
|
90
90
|
return RimoriClient.instance;
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Resolves once no plugin operation that still has to emit on the event bus is in flight.
|
|
94
|
+
*
|
|
95
|
+
* The host (PluginRenderer) awaits this before tearing the plugin's event bus down, so a
|
|
96
|
+
* fire-and-forget mutation started just before the user navigated away still gets its event
|
|
97
|
+
* delivered. Extend this if another module ever gains an emit that happens *after* an await.
|
|
98
|
+
*/
|
|
99
|
+
async whenIdle() {
|
|
100
|
+
await this.exercise.whenIdle();
|
|
101
|
+
}
|
|
92
102
|
navigation = {
|
|
93
103
|
toDashboard: () => {
|
|
94
104
|
this.event.emit('global.navigation.triggerToDashboard');
|
|
@@ -35,6 +35,12 @@ export interface ToolInvocation {
|
|
|
35
35
|
toolCallId: string;
|
|
36
36
|
toolName: string;
|
|
37
37
|
args: Record<string, string>;
|
|
38
|
+
/**
|
|
39
|
+
* The tool's own return value, recorded locally right after `execute()` resolves (see the
|
|
40
|
+
* `tool:` branch in `streamObject`). Optional so a call whose execution failed still
|
|
41
|
+
* round-trips as a call without a result rather than being dropped entirely.
|
|
42
|
+
*/
|
|
43
|
+
result?: unknown;
|
|
38
44
|
}
|
|
39
45
|
export interface Message {
|
|
40
46
|
id?: string;
|
|
@@ -43,6 +49,57 @@ export interface Message {
|
|
|
43
49
|
toolCalls?: ToolInvocation[];
|
|
44
50
|
}
|
|
45
51
|
export type OnLLMResponse = (id: string, response: string, finished: boolean, toolInvocations?: ToolInvocation[]) => void;
|
|
52
|
+
/** Wire-format tool-call content part of an `assistant` message — mirrors the AI SDK's `ToolCallPart`. */
|
|
53
|
+
export interface ToolCallContentPart {
|
|
54
|
+
type: 'tool-call';
|
|
55
|
+
toolCallId: string;
|
|
56
|
+
toolName: string;
|
|
57
|
+
input: unknown;
|
|
58
|
+
}
|
|
59
|
+
/** Wire-format tagged output envelope the AI SDK requires on a tool result (bare values are rejected). */
|
|
60
|
+
export type ToolResultOutput = {
|
|
61
|
+
type: 'text';
|
|
62
|
+
value: string;
|
|
63
|
+
} | {
|
|
64
|
+
type: 'json';
|
|
65
|
+
value: unknown;
|
|
66
|
+
};
|
|
67
|
+
/** Wire-format tool-result content part of a `tool` message — mirrors the AI SDK's `ToolResultPart`. */
|
|
68
|
+
export interface ToolResultContentPart {
|
|
69
|
+
type: 'tool-result';
|
|
70
|
+
toolCallId: string;
|
|
71
|
+
toolName: string;
|
|
72
|
+
output: ToolResultOutput;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A message as it travels over the wire to `/ai/llm` — this is what the backend's `MessageDto`
|
|
76
|
+
* accepts. Distinct from `Message` (the client-side chat-history shape) because a single
|
|
77
|
+
* `Message` with N tool calls expands into 2N+1 wire messages (see `toModelMessages`).
|
|
78
|
+
*/
|
|
79
|
+
export interface WireMessage {
|
|
80
|
+
id?: string;
|
|
81
|
+
role: 'user' | 'assistant' | 'system' | 'tool';
|
|
82
|
+
content: string | ToolCallContentPart[] | ToolResultContentPart[];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Expands client-side chat history into the flat wire format the backend/AI SDK expects.
|
|
86
|
+
* Every tool call on an assistant message becomes its own `assistant(tool-call)` +
|
|
87
|
+
* `tool(tool-result)` pair, in call order, immediately adjacent — providers reject a `tool`
|
|
88
|
+
* message that doesn't immediately follow the assistant message that issued its call, and the
|
|
89
|
+
* client has no reliable way to know the backend's original step boundaries, so 1:1 pairing is
|
|
90
|
+
* the only ordering that is always valid (the only fidelity loss is that calls that were
|
|
91
|
+
* genuinely parallel within one step replay as sequential ones, which is semantically
|
|
92
|
+
* irrelevant to the model).
|
|
93
|
+
*
|
|
94
|
+
* An assistant row with neither visible text nor tool calls is dropped — the AI SDK's message
|
|
95
|
+
* schema rejects it (this is the crash `UseChatHook.ts`'s `append` already filters out at the
|
|
96
|
+
* state layer; this is the second, defense-in-depth place, since `BuddyAssistant` does not
|
|
97
|
+
* filter its own state).
|
|
98
|
+
*
|
|
99
|
+
* Exported for direct unit testing — this is pure and the cheapest place to cover the wire
|
|
100
|
+
* format's edge cases (see packages/rimori-client/test).
|
|
101
|
+
*/
|
|
102
|
+
export declare function toModelMessages(history: Message[]): WireMessage[];
|
|
46
103
|
/**
|
|
47
104
|
* Controller for AI-related operations.
|
|
48
105
|
* Provides access to text generation, voice synthesis, and object generation.
|
|
@@ -1,3 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many of the most recent tool-calling turns keep their full tool-result payload when
|
|
3
|
+
* serialized. Older turns keep the call (so the model still "remembers that it looked") but
|
|
4
|
+
* have their result replaced with a short placeholder — tool results are the fastest-growing
|
|
5
|
+
* part of context (see ai.service.ts's per-step inputTokens log), and after this fix they
|
|
6
|
+
* accumulate for the whole conversation instead of being forgotten every turn, so trimming is
|
|
7
|
+
* load-bearing, not a nicety.
|
|
8
|
+
*/
|
|
9
|
+
const FULL_FIDELITY_TOOL_RESULT_TURNS = 3;
|
|
10
|
+
function toToolResultOutput(result, keepFull) {
|
|
11
|
+
if (!keepFull) {
|
|
12
|
+
return {
|
|
13
|
+
type: 'text',
|
|
14
|
+
value: '(result omitted to save context — this tool was already called earlier in the conversation)',
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (result === undefined)
|
|
18
|
+
return { type: 'text', value: '(no result recorded)' };
|
|
19
|
+
if (typeof result === 'string')
|
|
20
|
+
return { type: 'text', value: result };
|
|
21
|
+
return { type: 'json', value: result };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Expands client-side chat history into the flat wire format the backend/AI SDK expects.
|
|
25
|
+
* Every tool call on an assistant message becomes its own `assistant(tool-call)` +
|
|
26
|
+
* `tool(tool-result)` pair, in call order, immediately adjacent — providers reject a `tool`
|
|
27
|
+
* message that doesn't immediately follow the assistant message that issued its call, and the
|
|
28
|
+
* client has no reliable way to know the backend's original step boundaries, so 1:1 pairing is
|
|
29
|
+
* the only ordering that is always valid (the only fidelity loss is that calls that were
|
|
30
|
+
* genuinely parallel within one step replay as sequential ones, which is semantically
|
|
31
|
+
* irrelevant to the model).
|
|
32
|
+
*
|
|
33
|
+
* An assistant row with neither visible text nor tool calls is dropped — the AI SDK's message
|
|
34
|
+
* schema rejects it (this is the crash `UseChatHook.ts`'s `append` already filters out at the
|
|
35
|
+
* state layer; this is the second, defense-in-depth place, since `BuddyAssistant` does not
|
|
36
|
+
* filter its own state).
|
|
37
|
+
*
|
|
38
|
+
* Exported for direct unit testing — this is pure and the cheapest place to cover the wire
|
|
39
|
+
* format's edge cases (see packages/rimori-client/test).
|
|
40
|
+
*/
|
|
41
|
+
export function toModelMessages(history) {
|
|
42
|
+
const toolCallRowIndices = history
|
|
43
|
+
.map((m, i) => (m.toolCalls && m.toolCalls.length > 0 ? i : -1))
|
|
44
|
+
.filter((i) => i !== -1);
|
|
45
|
+
const fullFidelityIndices = new Set(toolCallRowIndices.slice(-FULL_FIDELITY_TOOL_RESULT_TURNS));
|
|
46
|
+
const out = [];
|
|
47
|
+
history.forEach((message, index) => {
|
|
48
|
+
const hasToolCalls = !!message.toolCalls && message.toolCalls.length > 0;
|
|
49
|
+
const hasText = typeof message.content === 'string' && message.content.length > 0;
|
|
50
|
+
if (!hasToolCalls) {
|
|
51
|
+
// A user row is always kept even if somehow empty; a text-less, tool-call-less
|
|
52
|
+
// assistant/system row is exactly the schema-invalid shape called out above.
|
|
53
|
+
if (!hasText && message.role !== 'user')
|
|
54
|
+
return;
|
|
55
|
+
out.push({ role: message.role, content: message.content });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const keepFullResults = fullFidelityIndices.has(index);
|
|
59
|
+
for (const call of message.toolCalls) {
|
|
60
|
+
out.push({
|
|
61
|
+
role: 'assistant',
|
|
62
|
+
content: [{ type: 'tool-call', toolCallId: call.toolCallId, toolName: call.toolName, input: call.args }],
|
|
63
|
+
});
|
|
64
|
+
out.push({
|
|
65
|
+
role: 'tool',
|
|
66
|
+
content: [
|
|
67
|
+
{
|
|
68
|
+
type: 'tool-result',
|
|
69
|
+
toolCallId: call.toolCallId,
|
|
70
|
+
toolName: call.toolName,
|
|
71
|
+
output: toToolResultOutput(call.result, keepFullResults),
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (hasText) {
|
|
77
|
+
out.push({ role: 'assistant', content: message.content });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
1
82
|
/**
|
|
2
83
|
* Controller for AI-related operations.
|
|
3
84
|
* Provides access to text generation, voice synthesis, and object generation.
|
|
@@ -55,14 +136,14 @@ export class AIModule {
|
|
|
55
136
|
*/
|
|
56
137
|
async getText(params) {
|
|
57
138
|
const { messages, tools, cache = false, prompt, variables } = params;
|
|
58
|
-
const {
|
|
139
|
+
const { data } = await this.streamObject({
|
|
59
140
|
cache,
|
|
60
141
|
tools,
|
|
61
142
|
messages,
|
|
62
143
|
prompt,
|
|
63
144
|
variables,
|
|
64
145
|
});
|
|
65
|
-
return result;
|
|
146
|
+
return data.result;
|
|
66
147
|
}
|
|
67
148
|
/**
|
|
68
149
|
* Stream text generation from messages using AI.
|
|
@@ -76,7 +157,7 @@ export class AIModule {
|
|
|
76
157
|
async getStreamedText(params) {
|
|
77
158
|
const { messages, onMessage, tools, cache = false, prompt, variables, } = params;
|
|
78
159
|
const messageId = Math.random().toString(36).substring(3);
|
|
79
|
-
const {
|
|
160
|
+
const { data, toolExchanges } = await this.streamObject({
|
|
80
161
|
cache,
|
|
81
162
|
tools,
|
|
82
163
|
messages,
|
|
@@ -84,8 +165,10 @@ export class AIModule {
|
|
|
84
165
|
variables,
|
|
85
166
|
onResult: ({ result }) => onMessage(messageId, result, false),
|
|
86
167
|
});
|
|
87
|
-
|
|
88
|
-
|
|
168
|
+
// Only the final callback carries the turn's tool exchanges — intermediate `finished:
|
|
169
|
+
// false` calls fire on every partial chunk and should not churn the caller's tool state.
|
|
170
|
+
onMessage(messageId, data.result, true, toolExchanges.length > 0 ? toolExchanges : undefined);
|
|
171
|
+
return data.result;
|
|
89
172
|
}
|
|
90
173
|
/**
|
|
91
174
|
* Generate voice audio from text using AI.
|
|
@@ -184,13 +267,14 @@ export class AIModule {
|
|
|
184
267
|
*/
|
|
185
268
|
async getObject(params) {
|
|
186
269
|
const { cache = false, tools = [], prompt, variables } = params;
|
|
187
|
-
|
|
270
|
+
const { data } = await this.streamObject({
|
|
188
271
|
messages: [],
|
|
189
272
|
cache,
|
|
190
273
|
tools,
|
|
191
274
|
prompt,
|
|
192
275
|
variables,
|
|
193
276
|
});
|
|
277
|
+
return data;
|
|
194
278
|
}
|
|
195
279
|
/**
|
|
196
280
|
* Generate a streamed structured object from a request using AI.
|
|
@@ -205,7 +289,7 @@ export class AIModule {
|
|
|
205
289
|
*/
|
|
206
290
|
async getStreamedObject(params) {
|
|
207
291
|
const { onResult, cache = false, tools = [], prompt, variables, settledOnly = false } = params;
|
|
208
|
-
|
|
292
|
+
const { data } = await this.streamObject({
|
|
209
293
|
messages: [],
|
|
210
294
|
onResult,
|
|
211
295
|
cache,
|
|
@@ -214,13 +298,21 @@ export class AIModule {
|
|
|
214
298
|
variables,
|
|
215
299
|
settledOnly,
|
|
216
300
|
});
|
|
301
|
+
return data;
|
|
217
302
|
}
|
|
218
303
|
async streamObject(params) {
|
|
219
304
|
const { messages, onResult = () => null, cache = false, tools = [], prompt, variables, settledOnly = false, } = params;
|
|
220
|
-
|
|
305
|
+
// toModelMessages expands any tool calls in history into their own assistant/tool wire
|
|
306
|
+
// messages first (C9: ids are positional, so they must be assigned after expansion).
|
|
307
|
+
const chatMessages = toModelMessages(messages).map((message, index) => ({
|
|
221
308
|
...message,
|
|
222
309
|
id: `${index + 1}`,
|
|
223
310
|
}));
|
|
311
|
+
// Records each tool call this request makes together with the result the client computed
|
|
312
|
+
// for it, so the caller (getStreamedText → BuddyAssistant/useChat) can persist the pair into
|
|
313
|
+
// chat history — this is the only place that ever sees both halves together (see the `tool:`
|
|
314
|
+
// branch below).
|
|
315
|
+
const exchanges = [];
|
|
224
316
|
const payload = {
|
|
225
317
|
cache,
|
|
226
318
|
tools,
|
|
@@ -284,7 +376,7 @@ export class AIModule {
|
|
|
284
376
|
isLoading = false;
|
|
285
377
|
onResult(currentObject, false);
|
|
286
378
|
logFinalResult();
|
|
287
|
-
return currentObject;
|
|
379
|
+
return { data: currentObject, toolExchanges: exchanges };
|
|
288
380
|
}
|
|
289
381
|
//the check needs to be behind readerDone because in closed connections the value is undefined
|
|
290
382
|
if (!value)
|
|
@@ -333,7 +425,7 @@ export class AIModule {
|
|
|
333
425
|
isLoading = false;
|
|
334
426
|
onResult(currentObject, false);
|
|
335
427
|
logFinalResult();
|
|
336
|
-
return currentObject;
|
|
428
|
+
return { data: currentObject, toolExchanges: exchanges };
|
|
337
429
|
}
|
|
338
430
|
if (command === 'data:') {
|
|
339
431
|
currentObject = JSON.parse(dataStr);
|
|
@@ -344,6 +436,7 @@ export class AIModule {
|
|
|
344
436
|
const tool = tools.find((tool) => tool.name === toolName);
|
|
345
437
|
if (tool && tool.execute) {
|
|
346
438
|
const result = await tool.execute(args);
|
|
439
|
+
exchanges.push({ toolCallId, toolName, args, result });
|
|
347
440
|
// Send the result to the backend
|
|
348
441
|
await this.sendToolResult(toolCallId, result);
|
|
349
442
|
}
|
|
@@ -366,12 +459,23 @@ export class AIModule {
|
|
|
366
459
|
}
|
|
367
460
|
}
|
|
368
461
|
}
|
|
369
|
-
return currentObject;
|
|
462
|
+
return { data: currentObject, toolExchanges: exchanges };
|
|
370
463
|
}
|
|
371
464
|
async sendToolResult(toolCallId, result) {
|
|
465
|
+
// A tool returning nothing used to be sent to the backend as the literal '[DONE]' sentinel,
|
|
466
|
+
// which the backend treats as an explicit "stop the conversation" signal and throws —
|
|
467
|
+
// turning a small tool bug into a dead chat. No tool in this codebase ever intentionally
|
|
468
|
+
// returns undefined to end the conversation, so instead send a non-empty error string the
|
|
469
|
+
// model can react to (e.g. apologize and retry) rather than silently killing the turn.
|
|
470
|
+
if (result === undefined || result === null || result === '') {
|
|
471
|
+
console.error(`Tool call ${toolCallId} returned an empty result — sending a fallback error instead.`);
|
|
472
|
+
}
|
|
473
|
+
const safeResult = result === undefined || result === null || result === ''
|
|
474
|
+
? 'The tool call failed to produce a result. Tell the user something went wrong and suggest they try again.'
|
|
475
|
+
: result;
|
|
372
476
|
await this.controller.fetchBackend('/ai/llm/tool_result', {
|
|
373
477
|
method: 'POST',
|
|
374
|
-
body: JSON.stringify({ toolCallId, result:
|
|
478
|
+
body: JSON.stringify({ toolCallId, result: safeResult }),
|
|
375
479
|
});
|
|
376
480
|
}
|
|
377
481
|
}
|
|
@@ -1,55 +1,101 @@
|
|
|
1
1
|
import { SupabaseClient } from '../CommunicationHandler';
|
|
2
2
|
import { RimoriCommunicationHandler, RimoriInfo } from '../CommunicationHandler';
|
|
3
3
|
import { EventModule } from './EventModule';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
end_date: string;
|
|
11
|
-
trigger_action: TriggerAction;
|
|
12
|
-
name: string;
|
|
13
|
-
description: string;
|
|
14
|
-
estimated_duration: number;
|
|
15
|
-
achievement_topic: string;
|
|
16
|
-
}
|
|
17
|
-
export interface Exercise {
|
|
4
|
+
/**
|
|
5
|
+
* A row of `public.exercises` (see planning/exercise-system-simplification.md). Flat
|
|
6
|
+
* `plugin_id`/`action_key`/`params` replace the old `attributes` jsonb blob, and `status`
|
|
7
|
+
* replaces the achievement_event_log anti-join.
|
|
8
|
+
*/
|
|
9
|
+
export interface ExerciseRow {
|
|
18
10
|
id: string;
|
|
11
|
+
user_id: string;
|
|
19
12
|
plugin_id: string;
|
|
13
|
+
action_key: string;
|
|
14
|
+
params: Record<string, string | number | boolean | undefined>;
|
|
15
|
+
name: string | null;
|
|
16
|
+
description: string | null;
|
|
20
17
|
start_date: string;
|
|
21
18
|
end_date: string;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
status: 'pending' | 'completed' | 'expired';
|
|
20
|
+
completed_at: string | null;
|
|
21
|
+
created_at: string;
|
|
22
|
+
}
|
|
23
|
+
/** Body of `POST /exercises` — no achievement_topic, no skill category, no reward fields. */
|
|
24
|
+
export interface CreateExerciseRowParams {
|
|
25
|
+
pluginId: string;
|
|
26
|
+
actionKey: string;
|
|
27
|
+
params?: Record<string, string | number | boolean | undefined>;
|
|
28
|
+
name?: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
/** ISO 8601 */
|
|
31
|
+
startDate: string;
|
|
32
|
+
/** ISO 8601 */
|
|
33
|
+
endDate: string;
|
|
29
34
|
}
|
|
30
35
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
36
|
+
* The kind of evidence an exercise page produces. Constant per page — never calculated from
|
|
37
|
+
* the run's outcome — because it describes what the interaction *is* (recognising a word vs.
|
|
38
|
+
* producing one), not how well the user did.
|
|
39
|
+
*/
|
|
40
|
+
export type EvidenceKind = 'recognition' | 'comprehension' | 'controlled' | 'production_written' | 'production_spoken';
|
|
41
|
+
/** Body of `POST /exercises/:id/complete` — measurements only, the id identifies the row. */
|
|
42
|
+
export interface CompleteExerciseParams {
|
|
43
|
+
errorRatio?: number;
|
|
44
|
+
durationMinutes?: number;
|
|
45
|
+
meta?: Record<string, unknown>;
|
|
46
|
+
/** The kind of evidence this result rests on. Required — a constant per exercise page. */
|
|
47
|
+
evidenceKind: EvidenceKind;
|
|
48
|
+
/** How many cards/questions/sentences the result rests on. */
|
|
49
|
+
itemCount?: number;
|
|
50
|
+
/** Named part-scores (0..1) where the page computes several and would otherwise discard them. */
|
|
51
|
+
components?: Record<string, number>;
|
|
52
|
+
}
|
|
53
|
+
/** Supabase-style result, per the repo's error-handling convention. */
|
|
54
|
+
type Result<T> = {
|
|
55
|
+
data: T;
|
|
56
|
+
error?: undefined;
|
|
57
|
+
} | {
|
|
58
|
+
data?: undefined;
|
|
59
|
+
error: Error;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Controller for exercise-related operations: reading the current week's open exercises,
|
|
63
|
+
* creating them, and marking them complete.
|
|
33
64
|
*/
|
|
34
65
|
export declare class ExerciseModule {
|
|
35
66
|
private supabase;
|
|
36
67
|
private communicationHandler;
|
|
37
68
|
private eventModule;
|
|
69
|
+
/**
|
|
70
|
+
* Mutations that still have to emit on the event bus after their backend round trip.
|
|
71
|
+
* The host tears the plugin's event bus down on unmount, so it drains this set first
|
|
72
|
+
* (see `whenIdle`) — otherwise a completion whose response lands after the user
|
|
73
|
+
* navigated away emits into a cleared bus and the dashboard never refetches.
|
|
74
|
+
*/
|
|
75
|
+
private pending;
|
|
38
76
|
constructor(supabase: SupabaseClient, communicationHandler: RimoriCommunicationHandler, _info: RimoriInfo, eventModule: EventModule);
|
|
39
77
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
78
|
+
* Registers a mutation as in-flight for the duration of its promise. Every tracked method
|
|
79
|
+
* emits its bus event as the last statement before resolving, so a caller awaiting
|
|
80
|
+
* `whenIdle()` is guaranteed to run *after* the emit, not merely after the HTTP response.
|
|
81
|
+
*/
|
|
82
|
+
private track;
|
|
83
|
+
/**
|
|
84
|
+
* Resolves once no mutation that still has to emit on the event bus is in flight.
|
|
85
|
+
* Loops because a tracked mutation may itself start another one while draining.
|
|
86
|
+
*/
|
|
87
|
+
whenIdle(): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Reads the current user's open exercises from the `weekly_exercises` view
|
|
90
|
+
* (`status = 'pending'`, inside this week's date window).
|
|
43
91
|
*/
|
|
44
|
-
view(): Promise<
|
|
92
|
+
view(): Promise<Result<ExerciseRow[]>>;
|
|
45
93
|
/**
|
|
46
|
-
* Creates one
|
|
47
|
-
*
|
|
48
|
-
* either all succeed or none are inserted.
|
|
49
|
-
* @param params Exercise creation parameters (single or array).
|
|
50
|
-
* @returns Created exercise objects.
|
|
94
|
+
* Creates one exercise. Single-row only — callers that create a batch fan out, so a partial
|
|
95
|
+
* failure is possible (the old achievements-backed bulk endpoint was all-or-nothing).
|
|
51
96
|
*/
|
|
52
|
-
add(params:
|
|
97
|
+
add(params: CreateExerciseRowParams): Promise<Result<ExerciseRow>>;
|
|
98
|
+
private addInternal;
|
|
53
99
|
/**
|
|
54
100
|
* Requests a new exercise session token from rimori-main.
|
|
55
101
|
* Use this for self-initiated exercises (user navigated to plugin via navbar and clicked Start).
|
|
@@ -67,12 +113,15 @@ export declare class ExerciseModule {
|
|
|
67
113
|
knowledgeId?: string;
|
|
68
114
|
}): Promise<void>;
|
|
69
115
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* @returns Success status.
|
|
116
|
+
* Marks an exercise completed. The measurements are reported on the completion but are not
|
|
117
|
+
* stored on the exercise row — they travel on the backend's `exercise.completed` event.
|
|
73
118
|
*/
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
119
|
+
complete(exerciseId: string, params: CompleteExerciseParams): Promise<Result<ExerciseRow>>;
|
|
120
|
+
private completeInternal;
|
|
121
|
+
/** Deletes an exercise. */
|
|
122
|
+
delete(id: string): Promise<Result<{
|
|
123
|
+
success: true;
|
|
124
|
+
}>>;
|
|
125
|
+
private deleteInternal;
|
|
78
126
|
}
|
|
127
|
+
export {};
|
|
@@ -1,48 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Controller for exercise-related operations
|
|
3
|
-
*
|
|
2
|
+
* Controller for exercise-related operations: reading the current week's open exercises,
|
|
3
|
+
* creating them, and marking them complete.
|
|
4
4
|
*/
|
|
5
5
|
export class ExerciseModule {
|
|
6
6
|
supabase;
|
|
7
7
|
communicationHandler;
|
|
8
8
|
eventModule;
|
|
9
|
+
/**
|
|
10
|
+
* Mutations that still have to emit on the event bus after their backend round trip.
|
|
11
|
+
* The host tears the plugin's event bus down on unmount, so it drains this set first
|
|
12
|
+
* (see `whenIdle`) — otherwise a completion whose response lands after the user
|
|
13
|
+
* navigated away emits into a cleared bus and the dashboard never refetches.
|
|
14
|
+
*/
|
|
15
|
+
pending = new Set();
|
|
9
16
|
constructor(supabase, communicationHandler, _info, eventModule) {
|
|
10
17
|
this.supabase = supabase;
|
|
11
18
|
this.communicationHandler = communicationHandler;
|
|
12
19
|
this.eventModule = eventModule;
|
|
13
20
|
}
|
|
14
21
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
22
|
+
* Registers a mutation as in-flight for the duration of its promise. Every tracked method
|
|
23
|
+
* emits its bus event as the last statement before resolving, so a caller awaiting
|
|
24
|
+
* `whenIdle()` is guaranteed to run *after* the emit, not merely after the HTTP response.
|
|
25
|
+
*/
|
|
26
|
+
track(operation) {
|
|
27
|
+
this.pending.add(operation);
|
|
28
|
+
// Swallow here only to keep the bookkeeping alive — the original promise is returned
|
|
29
|
+
// untouched, so the caller still sees any rejection.
|
|
30
|
+
void operation.catch(() => { }).finally(() => this.pending.delete(operation));
|
|
31
|
+
return operation;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolves once no mutation that still has to emit on the event bus is in flight.
|
|
35
|
+
* Loops because a tracked mutation may itself start another one while draining.
|
|
36
|
+
*/
|
|
37
|
+
async whenIdle() {
|
|
38
|
+
while (this.pending.size > 0) {
|
|
39
|
+
await Promise.allSettled([...this.pending]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Reads the current user's open exercises from the `weekly_exercises` view
|
|
44
|
+
* (`status = 'pending'`, inside this week's date window).
|
|
18
45
|
*/
|
|
19
46
|
async view() {
|
|
20
47
|
const { data, error } = await this.supabase.schema('public').from('weekly_exercises').select('*');
|
|
21
48
|
if (error) {
|
|
22
|
-
|
|
49
|
+
return { error: new Error(`Failed to fetch weekly exercises: ${error.message}`) };
|
|
23
50
|
}
|
|
24
|
-
return data
|
|
51
|
+
return { data: (data ?? []) };
|
|
25
52
|
}
|
|
26
53
|
/**
|
|
27
|
-
* Creates one
|
|
28
|
-
*
|
|
29
|
-
* either all succeed or none are inserted.
|
|
30
|
-
* @param params Exercise creation parameters (single or array).
|
|
31
|
-
* @returns Created exercise objects.
|
|
54
|
+
* Creates one exercise. Single-row only — callers that create a batch fan out, so a partial
|
|
55
|
+
* failure is possible (the old achievements-backed bulk endpoint was all-or-nothing).
|
|
32
56
|
*/
|
|
33
57
|
async add(params) {
|
|
34
|
-
|
|
58
|
+
return this.track(this.addInternal(params));
|
|
59
|
+
}
|
|
60
|
+
async addInternal(params) {
|
|
35
61
|
const response = await this.communicationHandler.fetchBackend('/exercises', {
|
|
36
62
|
method: 'POST',
|
|
37
|
-
body: JSON.stringify(
|
|
63
|
+
body: JSON.stringify(params),
|
|
38
64
|
});
|
|
39
65
|
if (!response.ok) {
|
|
40
|
-
|
|
41
|
-
throw new Error(`Failed to create exercises: ${errorText}`);
|
|
66
|
+
return { error: new Error(`Failed to create exercise: ${await response.text()}`) };
|
|
42
67
|
}
|
|
43
68
|
const data = await response.json();
|
|
44
69
|
this.eventModule.emit('global.exercises.triggerChange');
|
|
45
|
-
return data;
|
|
70
|
+
return { data };
|
|
46
71
|
}
|
|
47
72
|
/**
|
|
48
73
|
* Requests a new exercise session token from rimori-main.
|
|
@@ -73,19 +98,36 @@ export class ExerciseModule {
|
|
|
73
98
|
});
|
|
74
99
|
}
|
|
75
100
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* @returns Success status.
|
|
101
|
+
* Marks an exercise completed. The measurements are reported on the completion but are not
|
|
102
|
+
* stored on the exercise row — they travel on the backend's `exercise.completed` event.
|
|
79
103
|
*/
|
|
104
|
+
async complete(exerciseId, params) {
|
|
105
|
+
return this.track(this.completeInternal(exerciseId, params));
|
|
106
|
+
}
|
|
107
|
+
async completeInternal(exerciseId, params) {
|
|
108
|
+
const response = await this.communicationHandler.fetchBackend(`/exercises/${exerciseId}/complete`, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
body: JSON.stringify(params),
|
|
111
|
+
});
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
return { error: new Error(`Failed to complete exercise: ${await response.text()}`) };
|
|
114
|
+
}
|
|
115
|
+
const data = await response.json();
|
|
116
|
+
this.eventModule.emit('global.exercises.triggerChange');
|
|
117
|
+
return { data };
|
|
118
|
+
}
|
|
119
|
+
/** Deletes an exercise. */
|
|
80
120
|
async delete(id) {
|
|
121
|
+
return this.track(this.deleteInternal(id));
|
|
122
|
+
}
|
|
123
|
+
async deleteInternal(id) {
|
|
81
124
|
const response = await this.communicationHandler.fetchBackend(`/exercises/${id}`, {
|
|
82
125
|
method: 'DELETE',
|
|
83
126
|
});
|
|
84
127
|
if (!response.ok) {
|
|
85
|
-
|
|
86
|
-
throw new Error(`Failed to delete exercise: ${errorText}`);
|
|
128
|
+
return { error: new Error(`Failed to delete exercise: ${await response.text()}`) };
|
|
87
129
|
}
|
|
88
130
|
this.eventModule.emit('global.exercises.triggerChange');
|
|
89
|
-
return
|
|
131
|
+
return { data: { success: true } };
|
|
90
132
|
}
|
|
91
133
|
}
|
|
@@ -212,4 +212,12 @@ export interface UserInfo {
|
|
|
212
212
|
* Undefined otherwise. Plugins decide how to use this on a per-feature basis.
|
|
213
213
|
*/
|
|
214
214
|
dialect?: string;
|
|
215
|
+
/**
|
|
216
|
+
* True only while this render was triggered by a shared-exercise link (`/s/:id`) whose
|
|
217
|
+
* visitor's own tier doesn't clear the plugin's gate. UI-only fast-path signal — see
|
|
218
|
+
* `useTierGate` and the `shared_link_id` reserved key on `MainPanelAction`. The backend
|
|
219
|
+
* independently re-verifies the exercise session's shared_link_id before allowing the
|
|
220
|
+
* actual AI call, so this flag grants no access on its own.
|
|
221
|
+
*/
|
|
222
|
+
via_shared_link?: boolean;
|
|
215
223
|
}
|