1- import type { Models } from "@earendil-works/pi-ai" ;
1+ import { getSupportedThinkingLevels , type Models } from "@earendil-works/pi-ai" ;
22import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context" ;
33import {
44 AgentDoc ,
@@ -36,6 +36,7 @@ import type {
3636 ChannelInfo ,
3737 Chat ,
3838 ChatId ,
39+ Effort ,
3940 Event ,
4041 Metadata ,
4142 ModelRef ,
@@ -88,6 +89,7 @@ export class Channel {
8889 #log: Log ;
8990 #metadata: Metadata ;
9091 #active = 0 ;
92+ #admissions = new Map < ChatId , Promise < unknown > > ( ) ;
9193 #listeners = new Set < { chat : ChatId ; send : Send } > ( ) ;
9294
9395 private constructor ( options : Options , harness : Harness , log : Log , value : Metadata ) {
@@ -237,6 +239,7 @@ export class Channel {
237239 if ( window && used ) chat . context = { used, window } ;
238240 if ( record . owner ) chat . parent = record . owner . conversationId ;
239241 if ( agent ?. model ) chat . model = agent . model ;
242+ chat . effort = agent ?. thinkingLevel || "off" ;
240243 if ( lane ) chat . lane = lane ;
241244 return chat ;
242245 } ) ) ;
@@ -318,6 +321,19 @@ export class Channel {
318321 }
319322
320323 async #ask( request : Extract < Request , { op : "ask" } > ) {
324+ const chat = request . chat || ROOT_CONVERSATION_ID ;
325+ const previous = this . #admissions. get ( chat ) || Promise . resolve ( ) ;
326+ // Selection and submission must stay together across concurrent clients.
327+ const pending = previous . catch ( ( ) => { } ) . then ( ( ) => this . #admit( request ) ) ;
328+ this . #admissions. set ( chat , pending ) ;
329+ try {
330+ return await pending ;
331+ } finally {
332+ if ( this . #admissions. get ( chat ) === pending ) this . #admissions. delete ( chat ) ;
333+ }
334+ }
335+
336+ async #admit( request : Extract < Request , { op : "ask" } > ) {
321337 this . #author( request . author ) ;
322338 room . checkImages ( request . images ) ;
323339 const settings = await this . #harness. snapshot ( SettingsDoc , context ) ;
@@ -326,7 +342,7 @@ export class Channel {
326342 throw new Error ( "Only the owner can invoke agents here" ) ;
327343 }
328344 const conversation = await this . #conversation( request . chat ) ;
329- await this . #select( conversation , request . model ) ;
345+ await this . #select( conversation , request . model , request . effort ) ;
330346 const submission = await conversation . submit ( {
331347 type : "input" ,
332348 content : room . content ( request . author , request . text , request . images ) ,
@@ -336,19 +352,33 @@ export class Channel {
336352 return { submission : submission . id } ;
337353 }
338354
339- /** A model applies to a whole run, so it cannot change while one is active. */
340- async #select( conversation : Conversation , requested : ModelRef | undefined ) {
341- const current = ( await conversation . agent ( context ) ) . model ;
355+ /** Model and effort apply to a whole run, never just its later turns. */
356+ async #select( conversation : Conversation , requested : ModelRef | undefined , effort ?: Effort ) {
357+ const agent = await conversation . agent ( context ) ;
358+ const current = agent . model ;
342359 const model = requested || current || await (
343360 this . #options. defaultModel ?.( ) || choose ( this . #options. models )
344361 ) ;
345- if ( ! this . #options. models . getModel ( model . provider , model . modelId ) ) {
362+ const selected = this . #options. models . getModel ( model . provider , model . modelId ) ;
363+ if ( ! selected ) {
346364 throw new Error ( `Unknown model ${ model . provider } /${ model . modelId } ` ) ;
347365 }
348- const same = current ?. provider === model . provider && current . modelId === model . modelId ;
366+ const levels = getSupportedThinkingLevels ( selected ) ;
367+ if ( effort !== undefined && effort !== "off" && ! levels . includes ( effort ) ) {
368+ throw new Error (
369+ `Unsupported reasoning effort ${ effort } for ${ model . provider } /${ model . modelId } ` ,
370+ ) ;
371+ }
372+ // Pi's "off" means omit reasoning, allowing provider defaults on always-thinking models.
373+ const thinkingLevel = effort
374+ || ( levels . includes ( agent . thinkingLevel ) ? agent . thinkingLevel : "off" ) ;
375+ const sameModel = current ?. provider === model . provider && current . modelId === model . modelId ;
376+ const same = sameModel && thinkingLevel === agent . thinkingLevel ;
349377 const live = await this . #harness. snapshot ( LiveDoc , conversation . id , context ) ;
350378 if ( ! same && live ?. run ) {
351- throw new Error ( "The chat is busy; stop it or wait before changing its model" ) ;
379+ throw new Error (
380+ "The chat is busy; stop it or wait before changing its model or reasoning effort" ,
381+ ) ;
352382 }
353383 const available = await this . #options. models . getAvailable ( model . provider ) ;
354384 if ( ! available . some ( ( value ) => value . id === model . modelId ) ) {
@@ -357,7 +387,7 @@ export class Channel {
357387 ) ;
358388 }
359389 if ( same ) return ;
360- await conversation . configure ( { model } , context ) ;
390+ await conversation . configure ( { model, thinkingLevel } , context ) ;
361391 }
362392
363393 async #chat( request : Extract < Request , { op : "chat" } > ) {
0 commit comments