mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 14:21:06 -10:00
App: a goal, a plan step and a /loop run without anyone there, so the agent's question is declined or skipped at once there and cannot stall them; normal chat still shows the question card (#4244)
This commit is contained in:
1 parent
a9a2939fe9
commit
2c2893081b
4 files changed
+25
-3
No files matched your search
@@ -634,6 +634,7 @@ cases.quote = async () => {
|
||||
await page.locator('textarea').first().fill('SCRIPT:echo thanks for the quote');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.locator('.sent-quote').first().waitFor({ timeout: 15000 });
|
||||
await page.waitForTimeout(300);
|
||||
const sent = await page.locator('.st-msg--user').last().innerText();
|
||||
check(/Second point: nothing has to be uploaded/.test(sent) && /SCRIPT:echo thanks for the quote/.test(sent) && !/Quoting:/.test(sent) && !/「/.test(sent), `${tag}: the sent message shows the quote apart from the reply, without the markup`, sent.replace(/\n+/g, ' | ').slice(0, 160));
|
||||
const got = worker.requests.at(-1)?.raw ?? '';
|
||||
@@ -705,6 +706,18 @@ cases.ask = async () => {
|
||||
check(page.problems.length === 0, `${tag}: no page error`, page.problems.join('; '));
|
||||
await page.context().close();
|
||||
}
|
||||
// A /loop runs without anyone there: the question is skipped at once, no card, and the loop is not left waiting.
|
||||
{
|
||||
const before = (await asked()).length;
|
||||
const page = await open('light', 1280, 860, [chat('s1', 'Loop', [])], { origin: t.origin });
|
||||
await page.locator('textarea').first().fill('/loop 10m Build me a page stop after 1 answer');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForFunction(async () => (await (await fetch('/__fixture/asked')).json()).length > 0, null, { timeout: 15000 }).catch(() => {});
|
||||
const got = (await asked()).slice(before).at(-1);
|
||||
check(got?.skip === true && got?.callId === 'call-ask-1', 'a loop: the question is skipped at once', JSON.stringify(got));
|
||||
check(await page.locator('.askq').count() === 0, 'a loop: no question card is shown');
|
||||
await page.context().close();
|
||||
}
|
||||
await fetch(`${t.origin}/__fixture/ask`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ on: false }) });
|
||||
};
|
||||
|
||||
|
||||
@@ -278,14 +278,15 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
|
||||
if (!r.callId) return;
|
||||
pending.set(`${value.agentId}:${r.callId}`, { eventId: value.eventId, sessionId: value.agentId, callId: r.callId, tool: r.toolName, reason: r.reason ?? null, at: Date.now(), clientId: runtime.clientId });
|
||||
} else if (value.type === 'waterfall' && value.event === 'user-questions/request') {
|
||||
// The agent of a chat that is running asks the person: the question goes to the page (the question card above the message box) and the
|
||||
// The agent of a chat that is running in front of the person asks the person: the question goes to the page (the question card above the message box) and the
|
||||
// Harness waits for the answer. Anything else (a helper's question, a chat that is stopping, a request with nothing to show) is declared
|
||||
// unanswerable, which makes the tool return an error the model reads and carries on from, instead of a turn that waits forever.
|
||||
const r = value.request ?? {};
|
||||
const chatId = chatOfSession(value.agentId);
|
||||
const turn = chatId ? chats.get(chatId)?.turn : null;
|
||||
const items = parseQuestions(r.questions);
|
||||
if (!turn || turn.finished || turn.stopping || !turn.announceQuestion || !items.length) {
|
||||
// A goal or a plan step runs without anyone there: a question would stall it, so it is declined at once (the model reads "could not be answered" and goes on).
|
||||
if (!turn || turn.finished || turn.stopping || turn.goalRun || turn.plan || !turn.announceQuestion || !items.length) {
|
||||
void runtime.gw.answerEvent({ clientId: runtime.clientId, eventId: value.eventId, outcome: 'next' }).catch(() => {});
|
||||
} else {
|
||||
const callId = String(r.wait?.callId ?? value.eventId);
|
||||
|
||||
@@ -639,6 +639,8 @@ function setAsks(sessionId: string, list: AgentAsk[]) {
|
||||
/** The agent asked the person something (a frame of the host), or the question ended without an answer. */
|
||||
export function addAgentAsk(sessionId: string, frame: { callId?: unknown; questions?: unknown }) {
|
||||
const ask = readAsk(frame);
|
||||
// A /loop runs without anyone there: the question is skipped at once, so the loop never waits for an answer.
|
||||
if (ask && loopScheduler.isRunning(sessionId)) { void answerAgentAsk(sessionId, ask.callId, null).catch(() => {}); return; }
|
||||
if (ask) setAsks(sessionId, [...(state.agentAsks[sessionId] ?? []).filter(a => a.callId !== ask.callId), ask]);
|
||||
}
|
||||
export function removeAgentAsk(sessionId: string, callId?: string) {
|
||||
|
||||
@@ -443,7 +443,7 @@ test('#4244 the tool is offered again, as the group "Questions for you", and the
|
||||
assert.match(catalog, /id: 'questions'[^\n]*plugins: \['tool-ask-user'\]/);
|
||||
const bridge = readFileSync(new URL('../server/agent/agent-bridge.mjs', import.meta.url), 'utf8');
|
||||
assert.match(bridge, /await rt\.gw\.answerEvent\(\{ clientId: entry\.clientId \?\? rt\.clientId, eventId: entry\.eventId, outcome \}\);/);
|
||||
assert.match(bridge, /if \(!turn \|\| turn\.finished \|\| turn\.stopping \|\| !turn\.announceQuestion \|\| !items\.length\) \{\s*void runtime\.gw\.answerEvent\(\{ clientId: runtime\.clientId, eventId: value\.eventId, outcome: 'next' \}\)/, 'a helper question, a stopping chat or an empty request is still declared unanswerable');
|
||||
assert.match(bridge, /if \(!turn \|\| turn\.finished \|\| turn\.stopping \|\| turn\.goalRun \|\| turn\.plan \|\| !turn\.announceQuestion \|\| !items\.length\) \{\s*void runtime\.gw\.answerEvent\(\{ clientId: runtime\.clientId, eventId: value\.eventId, outcome: 'next' \}\)/, 'a helper question, a stopping chat or an empty request is still declared unanswerable');
|
||||
const gateway = readFileSync(new URL('../server/agent/gateway-client.mjs', import.meta.url), 'utf8');
|
||||
assert.match(gateway, /const wire = outcome === 'next' \? \{ kind: 'next' \} : \{ kind: 'result', value: outcome \};/, 'a structured answer travels as the result value');
|
||||
assert.match(src('components/ApprovalCard.tsx'), /asks\.map\(a => <QuestionCard key=\{a\.callId\} sessionId=\{sessionId!\} ask=\{a\}\/>\)/);
|
||||
@@ -463,3 +463,9 @@ test('#3818 the transcript lets go of the newest token while the reader has word
|
||||
assert.match(chat, /if \(pinned\.current && readerHasSelection\(el\)\) \{\s*\/\/[^\n]*\n\s*pinned\.current = false;\s*setAwayFromBottom\(distanceFromBottom\(el\) > PILL_DISTANCE\);\s*\}\s*if \(pinned\.current\)\s*el\.scrollTop = el\.scrollHeight;/, 'the observer that follows a growing reply');
|
||||
assert.match(chat, /if \(pinned\.current && !sent && readerHasSelection\(el\)\) \{\s*pinned\.current = false;/, 'and the effect that follows each token; sending a message still brings the reader to it');
|
||||
});
|
||||
|
||||
test('#4244 a goal, a plan step and a loop run without anyone there, so a question is declined at once instead of waiting', () => {
|
||||
const bridge = readFileSync(new URL('../server/agent/agent-bridge.mjs', import.meta.url), 'utf8');
|
||||
assert.match(bridge, /if \(!turn \|\| turn\.finished \|\| turn\.stopping \|\| turn\.goalRun \|\| turn\.plan \|\| !turn\.announceQuestion \|\| !items\.length\) \{\s*void runtime\.gw\.answerEvent\(\{ clientId: runtime\.clientId, eventId: value\.eventId, outcome: 'next' \}\)/);
|
||||
assert.match(src('lib/store.ts'), /if \(ask && loopScheduler\.isRunning\(sessionId\)\) \{ void answerAgentAsk\(sessionId, ask\.callId, null\)\.catch\(\(\) => \{\}\); return; \}/);
|
||||
});
|
||||
Reference in new issue
Block a user