@leviyuan/lodestar 0.1.2 → 0.1.3
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/package.json +1 -1
- package/src/session.ts +85 -31
package/package.json
CHANGED
package/src/session.ts
CHANGED
|
@@ -76,11 +76,20 @@ export class Session {
|
|
|
76
76
|
private proc: ClaudeProcess | null = null
|
|
77
77
|
private currentTurn: TurnState | null = null
|
|
78
78
|
private pendingPermissions = new Map<string, { toolUseId: string }>()
|
|
79
|
-
/** Open AskUserQuestion tool calls — keyed by tool_use_id
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
|
|
79
|
+
/** Open AskUserQuestion tool calls — keyed by tool_use_id. The SDK
|
|
80
|
+
* routes AskUserQuestion through the can_use_tool flow even under
|
|
81
|
+
* bypass; we have to thread the permission `requestId` through here
|
|
82
|
+
* so onAskAnswer can answer it with `updatedInput.answers` populated.
|
|
83
|
+
* `deferredAnswer` covers the race where the user clicks a button
|
|
84
|
+
* BEFORE can_use_tool arrives (addTool fires on the assistant
|
|
85
|
+
* message; can_use_tool is a separate control_request that lands
|
|
86
|
+
* slightly later). */
|
|
87
|
+
private pendingAsks = new Map<string, {
|
|
88
|
+
questions: cards.AskQuestion[]
|
|
89
|
+
i: number
|
|
90
|
+
requestId?: string
|
|
91
|
+
deferredAnswer?: { questionIdx: number; optionIdx: number; user: string }
|
|
92
|
+
}>()
|
|
84
93
|
private turnCounter = 0
|
|
85
94
|
// Last seen sessionId — preserved across `kill`/`stop` so a later
|
|
86
95
|
// `restart` can resume the same Claude conversation even after the
|
|
@@ -317,40 +326,70 @@ export class Session {
|
|
|
317
326
|
}
|
|
318
327
|
}
|
|
319
328
|
|
|
320
|
-
/**
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
329
|
+
/** Click handler for an AskUserQuestion option button. The actual
|
|
330
|
+
* "send answer to Claude" plumbing lives in `resolveAsk` — this
|
|
331
|
+
* wrapper just chooses between answering immediately (the common
|
|
332
|
+
* case: can_use_tool has already arrived and parked a requestId on
|
|
333
|
+
* the pendingAsk record) versus deferring (the rare race where the
|
|
334
|
+
* user clicks before can_use_tool lands). */
|
|
325
335
|
async onAskAnswer(toolUseId: string, questionIdx: number, optionIdx: number, user: string): Promise<void> {
|
|
326
336
|
const pending = this.pendingAsks.get(toolUseId)
|
|
327
337
|
if (!pending) { log(`session "${this.sessionName}": stray ask answer for ${toolUseId}`); return }
|
|
338
|
+
if (pending.requestId) {
|
|
339
|
+
this.resolveAsk(toolUseId, pending.requestId, questionIdx, optionIdx, user)
|
|
340
|
+
} else {
|
|
341
|
+
// can_use_tool hasn't landed yet — park the click. renderPermission
|
|
342
|
+
// will drain it the moment the request arrives.
|
|
343
|
+
pending.deferredAnswer = { questionIdx, optionIdx, user }
|
|
344
|
+
log(`session "${this.sessionName}": ask answer deferred for ${toolUseId} (no requestId yet)`)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Settle an AskUserQuestion: emit the permission allow with the
|
|
349
|
+
* picked option folded into `updatedInput.answers` (this is the
|
|
350
|
+
* shape the SDK reads to synthesise the tool_result string), repaint
|
|
351
|
+
* the panel ✅, drop bookkeeping. Single source of truth — both the
|
|
352
|
+
* normal click path and the deferred-answer drain go through here. */
|
|
353
|
+
private resolveAsk(
|
|
354
|
+
toolUseId: string,
|
|
355
|
+
requestId: string,
|
|
356
|
+
questionIdx: number,
|
|
357
|
+
optionIdx: number,
|
|
358
|
+
user: string,
|
|
359
|
+
): void {
|
|
360
|
+
const pending = this.pendingAsks.get(toolUseId)
|
|
361
|
+
if (!pending) return
|
|
328
362
|
const q = pending.questions[questionIdx]
|
|
329
363
|
const opt = q?.options?.[optionIdx]
|
|
330
364
|
if (!q || !opt) {
|
|
331
365
|
log(`session "${this.sessionName}": ask answer out of range q=${questionIdx} o=${optionIdx}`)
|
|
332
366
|
return
|
|
333
367
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
//
|
|
368
|
+
const turn = this.currentTurn
|
|
369
|
+
const meta = turn?.toolByUseId.get(toolUseId)
|
|
370
|
+
const originalInput = meta?.input ?? {}
|
|
371
|
+
// SDK keys the answer record by the question's text — confirmed
|
|
372
|
+
// by the v0.1.2 jsonl trace (empty record formatted to "User has
|
|
373
|
+
// answered your questions: ."). Populate the key matching this
|
|
374
|
+
// question with the chosen option's label.
|
|
338
375
|
const answers: Record<string, string> = { [q.question]: opt.label }
|
|
339
|
-
|
|
340
|
-
|
|
376
|
+
this.proc?.sendPermissionResponse(requestId, 'allow', {
|
|
377
|
+
updatedInput: { ...originalInput, answers },
|
|
378
|
+
})
|
|
379
|
+
this.pendingPermissions.delete(requestId)
|
|
341
380
|
this.pendingAsks.delete(toolUseId)
|
|
342
381
|
|
|
343
|
-
// Repaint the panel to reflect the final choice — same element_id,
|
|
344
|
-
// status flips to ✅, body shows what was picked.
|
|
345
|
-
const turn = this.currentTurn
|
|
346
|
-
const meta = turn?.toolByUseId.get(toolUseId)
|
|
347
382
|
if (turn && meta) {
|
|
348
|
-
meta.output =
|
|
383
|
+
meta.output = JSON.stringify({ answers })
|
|
349
384
|
meta.isError = false
|
|
350
385
|
const resolvedNote = `\n\n✅ **已回答** by ${user || '匿名'}: ${opt.label}`
|
|
351
386
|
const el = cards.askUserQuestionElement(meta.i, toolUseId, pending.questions, '✅', resolvedNote)
|
|
352
387
|
void cardkit.replaceElement(turn.cardId, cards.ELEMENTS.tool(meta.i), el)
|
|
353
388
|
}
|
|
389
|
+
|
|
390
|
+
if (this.pendingPermissions.size === 0 && this.status === 'awaiting_permission') {
|
|
391
|
+
this.status = 'working'
|
|
392
|
+
}
|
|
354
393
|
}
|
|
355
394
|
|
|
356
395
|
async onConsoleAction(action: string): Promise<void> {
|
|
@@ -680,17 +719,32 @@ export class Session {
|
|
|
680
719
|
this.proc?.sendPermissionResponse(req.request_id, 'deny', { denyMessage: 'unknown tool_use_id' })
|
|
681
720
|
return
|
|
682
721
|
}
|
|
683
|
-
// AskUserQuestion:
|
|
684
|
-
//
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
//
|
|
688
|
-
//
|
|
689
|
-
//
|
|
690
|
-
//
|
|
691
|
-
// the
|
|
722
|
+
// AskUserQuestion: SDK routes it through can_use_tool even under
|
|
723
|
+
// bypass. The PAYLOAD of "user has answered" is the permission
|
|
724
|
+
// response itself — specifically `updatedInput.answers`. So we
|
|
725
|
+
// CANNOT auto-allow here (that's the v0.1.2 bug: SDK got an empty
|
|
726
|
+
// answers map and immediately synthesised a "User has answered
|
|
727
|
+
// your questions: ." tool_result). Park the requestId on the
|
|
728
|
+
// pendingAsk record and wait for the user to click an option;
|
|
729
|
+
// onAskAnswer will then send allow + updatedInput.answers in one
|
|
730
|
+
// shot. If the user already clicked between addTool and now —
|
|
731
|
+
// the deferredAnswer slot — settle immediately.
|
|
692
732
|
if (meta.name === 'AskUserQuestion') {
|
|
693
|
-
this.
|
|
733
|
+
const ask = this.pendingAsks.get(toolUseId)
|
|
734
|
+
if (!ask) {
|
|
735
|
+
// Defensive: addTool should have populated pendingAsks. If it
|
|
736
|
+
// didn't, fall back to a denial so Claude doesn't hang.
|
|
737
|
+
log(`session "${this.sessionName}": AskUserQuestion ${toolUseId} missing pendingAsk — deny`)
|
|
738
|
+
this.proc?.sendPermissionResponse(req.request_id, 'deny', { denyMessage: 'no pending ask' })
|
|
739
|
+
return
|
|
740
|
+
}
|
|
741
|
+
ask.requestId = req.request_id
|
|
742
|
+
this.pendingPermissions.set(req.request_id, { toolUseId })
|
|
743
|
+
if (ask.deferredAnswer) {
|
|
744
|
+
const { questionIdx, optionIdx, user } = ask.deferredAnswer
|
|
745
|
+
ask.deferredAnswer = undefined
|
|
746
|
+
this.resolveAsk(toolUseId, req.request_id, questionIdx, optionIdx, user)
|
|
747
|
+
}
|
|
694
748
|
return
|
|
695
749
|
}
|
|
696
750
|
this.status = 'awaiting_permission'
|