feat(learn): choosing how much an assistant thinks, and the assistants by provider in the field

Halkyon Learn's ADR 0021, D60 and D63:

- A chip after the scope's offers the efforts the operator allows for the
  assistant asked, its own marked; the one picked is kept per assistant on
  the device (DeviceSettings.askEfforts) and asked at, also by a
  conversation continued from the history. One the operator no longer
  allows counts as none.
- Every question asks for its effort's ceiling on output, as the thinking
  counts towards it (8,192 to 65,536 tokens; 16,384 at the model's own),
  where it asked for 4,096; the flashcards proposed, Explain and Why? ask
  at the assistant's own effort, a person's own key at none and 4,096.
- The field's picker lists the assistants under their providers' names,
  each in its colour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-26 16:05:49 +01:00
commit 8b5bdfed8c
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 474 additions and 50 deletions

View file

@ -80,6 +80,12 @@ void main() {
expect(assistant, 'claude');
expect(request.tools, [proposeCardsTool]);
expect(request.system, proposalPrompt());
expect(
request.effort,
isNull,
reason: "the assistant's own effort: nobody chose one here (D63)",
);
expect(request.maxOutputTokens, LlmEffort.defaultOutputCeiling);
expect(
(request.messages.single.parts.single as LlmText).text,
'Verbs: 食べる is to eat.',

View file

@ -131,6 +131,7 @@ void main() {
AssistantGateway? asking,
AssistantTask task = AssistantTask.ask,
String? title,
LlmEffort? effort,
}) => ConversationCubit(
assistants: AssistantRepository(asking ?? gateway),
lookups: lookups,
@ -141,6 +142,7 @@ void main() {
conversationId: conversationId,
task: task,
title: title,
effort: effort,
answerIn: 'en',
);
@ -182,7 +184,12 @@ void main() {
final (assistant, request) = gateway.asked.single;
expect(assistant, 'claude');
expect(request.maxOutputTokens, ConversationCubit.maxOutputTokens);
expect(
request.maxOutputTokens,
LlmEffort.defaultOutputCeiling,
reason: "the model's own effort thinks within the default ceiling",
);
expect(request.effort, isNull);
expect(request.tools, isEmpty, reason: 'only what is ticked goes (D50)');
expect(request.system, contains('nothing to look up'));
@ -195,6 +202,60 @@ void main() {
await conversation.close();
});
test('asks every question at the effort the person chose, with its '
'ceiling (D63)', () async {
const thinking = OfferedAssistant(
id: 'claude',
name: 'Claude',
protocol: 'anthropic',
host: 'api.anthropic.com',
model: 'claude-opus',
effort: LlmEffort.low,
efforts: [LlmEffort.low, LlmEffort.medium, LlmEffort.high],
);
final conversation = cubit(
choice: const GrantedAssistant(thinking),
effort: LlmEffort.high,
);
await conversation.open();
gateway.answers
..add(_says('Because it joins.'))
..add(_says('Yes.'));
await conversation.ask('Why て-form?', const []);
await conversation.ask('Always?', const []);
for (final (_, request) in gateway.asked) {
expect(request.effort, LlmEffort.high);
expect(request.maxOutputTokens, LlmEffort.high.outputCeiling);
}
expect(gateway.asked, hasLength(2));
await conversation.close();
});
test("without an effort chosen, it asks at the assistant's, within its "
'ceiling', () async {
const thinking = OfferedAssistant(
id: 'claude',
name: 'Claude',
protocol: 'anthropic',
host: 'api.anthropic.com',
model: 'claude-opus',
effort: LlmEffort.low,
efforts: [LlmEffort.low, LlmEffort.medium],
);
final conversation = cubit(choice: const GrantedAssistant(thinking));
await conversation.open();
gateway.answers.add(_says('Because it joins.'));
await conversation.ask('Why て-form?', const []);
final (_, request) = gateway.asked.single;
expect(request.effort, isNull, reason: 'the instance applies its own');
expect(request.maxOutputTokens, LlmEffort.low.outputCeiling);
await conversation.close();
});
test('sends the conversation so far with a follow-up, and keeps it in the '
'same conversation', () async {
final conversation = cubit();