Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license

This commit is contained in:
yilsem committed 2026-09-05 13:26:46 +03:00
commit 03634b1ca3
11340 files changed
+3442369

No files matched your search

+212
View File
@@ -0,0 +1,212 @@
import { atom, computed } from 'nanostores'
import { $gateway } from './gateway'
import { $activeSessionId } from './session'
export interface ClarifyQuestion {
/** Server-generated wire id (q0..qN) — clarify.respond keys answers by it. */
qid: string
question: string
choices: string[] | null
multiSelect: boolean
}
export interface ClarifyRequest {
requestId: string
question: string
choices: string[] | null
multiSelect: boolean
/** Local receipt time (Unix seconds), used to reject stale resume cleanup. */
receivedAt?: number
sessionId: string | null
/** Batch (multi-question) clarify: present instead of question/choices. */
questions?: ClarifyQuestion[]
/** Answers already locked server-side (reconnect replay): qid → answer. */
lockedAnswers?: Record<string, string>
}
/**
* The backend labels the agent's recommended option by appending this to the
* first choice (`tools/clarify_tool.py::mark_recommended`). The renderer never
* writes it — it only styles it, and discounts it when measuring a choice so a
* long option isn't dropped for length the label added.
*/
export const RECOMMENDED_LABEL = '(Recommended)'
export const bareChoice = (choice: string): string =>
choice.endsWith(RECOMMENDED_LABEL) ? choice.slice(0, -RECOMMENDED_LABEL.length).trim() : choice
/**
* Validate and normalize a choices array.
*
* Keeps non-blank, newline-free strings of length ≤ 200; drops everything else
* and returns an empty array when nothing usable survives — the caller then
* falls back to a free-text answer instead of dead buttons.
*/
export function normalizeChoices(choices: unknown): string[] {
if (!Array.isArray(choices)) {
return []
}
return choices.filter(
(c): c is string => typeof c === 'string' && c.trim().length > 0 && bareChoice(c).length <= 200 && !c.includes('\n')
)
}
/**
* Structured warning for a clarify payload that arrived with choices but had
* them all normalized away — keeps the remaining #69122 "no selectable choices"
* triggers diagnosable in the field without dead constant fields.
*/
export function warnDroppedChoices(source: 'gateway' | 'tool_args', question: string, rawChoices: unknown): void {
console.warn('[clarify] choices dropped after normalization', {
choices_count: Array.isArray(rawChoices) ? rawChoices.length : 0,
question_length: question.length,
source
})
}
/**
* Validate and normalize a batch clarify payload's `questions` array.
*
* Keeps entries with a non-blank string `qid` and `question`; per-question
* choices go through `normalizeChoices` (all-blank → open-ended) and
* multi_select is only honored alongside surviving choices. Returns an empty
* array when nothing usable remains — the caller treats that as "not a
* batch" instead of rendering an unanswerable form.
*/
export function normalizeQuestions(questions: unknown): ClarifyQuestion[] {
if (!Array.isArray(questions)) {
return []
}
const normalized: ClarifyQuestion[] = []
for (const entry of questions) {
if (typeof entry !== 'object' || entry === null) {
continue
}
const row = entry as Record<string, unknown>
const qid = typeof row.qid === 'string' ? row.qid.trim() : ''
const question = typeof row.question === 'string' ? row.question.trim() : ''
if (!qid || !question) {
continue
}
const choices = normalizeChoices(row.choices)
normalized.push({
choices: choices.length > 0 ? choices : null,
multiSelect: row.multi_select === true && choices.length > 0,
qid,
question
})
}
return normalized
}
// Pending clarify requests keyed by the runtime session id that raised them.
// Storing per-session (instead of one shared slot) lets a *background* session
// park its clarify request while the user is looking at a different chat, then
// resolve it once they switch over — without a second concurrent clarify
// clobbering the first. A request with no session id lands under the empty key.
const keyFor = (sessionId: string | null | undefined): string => sessionId ?? ''
export const $clarifyRequests = atom<Record<string, ClarifyRequest>>({})
// The clarify request for the currently-viewed session. The inline ClarifyTool
// only ever mounts inside the active session's transcript, so it reads this
// focus-scoped view rather than reaching into the whole map.
export const $clarifyRequest = computed(
[$clarifyRequests, $activeSessionId],
(requests, activeId) => requests[keyFor(activeId)] ?? null
)
/** The clarify request for one specific session — the tile counterpart of the
* active-session `$clarifyRequest` view (same map, fixed key). */
export const sessionClarifyRequest = (sessionId: string | null) =>
computed($clarifyRequests, requests => requests[keyFor(sessionId)] ?? null)
export function setClarifyRequest(request: ClarifyRequest): void {
$clarifyRequests.set({ ...$clarifyRequests.get(), [keyFor(request.sessionId)]: request })
}
export function clearClarifyRequest(requestId?: string, sessionId?: string | null): void {
const requests = $clarifyRequests.get()
// Targeted clear when the caller knows the session (the common path from the
// inline ClarifyTool answering its own request).
if (sessionId !== undefined) {
const key = keyFor(sessionId)
const current = requests[key]
if (!current || (requestId && current.requestId !== requestId)) {
return
}
const next = { ...requests }
delete next[key]
$clarifyRequests.set(next)
return
}
// Fallback with no session hint: drop every entry matching the request id
// (or clear all when none is given).
const next: Record<string, ClarifyRequest> = {}
let changed = false
for (const [key, value] of Object.entries(requests)) {
if (requestId && value.requestId !== requestId) {
next[key] = value
} else {
changed = true
}
}
if (changed) {
$clarifyRequests.set(next)
}
}
/** Whether `sessionId` has a clarify parked on it right now (imperative read —
* the composer checks this on Enter, not on every render). */
export const hasClarifyRequest = (sessionId: string | null | undefined): boolean =>
Boolean($clarifyRequests.get()[keyFor(sessionId)])
/**
* Answer `sessionId`'s pending clarify with an empty answer (a skip) and drop it
* locally, resolving to whether there was one to skip.
*
* The composer uses this when the user types a real message instead of picking
* an option: a clarify blocks the agent inside its tool batch, so leaving it
* unanswered would park the follow-up until the server-side clarify timeout
* (default 5 min) — the message looks sent and nothing happens. Skipping lets
* the tool return and the turn carry on with the user's actual words.
*
* An empty answer is the same thing the card's own Skip button sends, and
* `clarify.respond` is `allow_expired`, so racing the timeout is harmless.
*/
export async function skipClarifyRequest(sessionId: string | null | undefined): Promise<boolean> {
const request = $clarifyRequests.get()[keyFor(sessionId)]
if (!request) {
return false
}
// Clear first: the answer is already decided, and an in-flight RPC must not
// leave a live card the user can answer a second time.
clearClarifyRequest(request.requestId, request.sessionId)
try {
await $gateway.get()?.request('clarify.respond', { request_id: request.requestId, answer: '' })
} catch {
// The tool times out on its own; a failed skip must never swallow the
// message the user is actually sending.
}
return true
}