toolroll 0.9.95 → 0.9.97
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/CHANGELOG.md +13 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +32 -32
- package/dist/browser-workspace.d.ts +2 -0
- package/dist/card-render.d.ts +18 -1
- package/dist/card-render.js +40 -3
- package/dist/chat-delivery.d.ts +5 -1
- package/dist/chat-delivery.js +74 -25
- package/dist/chat-follow.d.ts +6 -3
- package/dist/chat-follow.js +23 -7
- package/dist/chat-live-schema.d.ts +17 -0
- package/dist/chat-live-schema.js +32 -0
- package/dist/chat-live.d.ts +161 -0
- package/dist/chat-live.js +491 -0
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +1 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-api.d.ts +3 -1
- package/dist/discord-api.js +9 -3
- package/dist/discord-chat.d.ts +13 -3
- package/dist/discord-chat.js +77 -21
- package/dist/discord.js +8 -1
- package/dist/lead-answer-eval.d.ts +86 -0
- package/dist/lead-answer-eval.js +255 -0
- package/dist/lead-cli-live.d.ts +56 -0
- package/dist/lead-cli-live.js +200 -0
- package/dist/lead-cli.d.ts +3 -0
- package/dist/lead-cli.js +34 -4
- package/dist/lead-latency-schema.d.ts +24 -0
- package/dist/lead-latency-schema.js +36 -0
- package/dist/lead-progress.d.ts +10 -1
- package/dist/lead-progress.js +1 -1
- package/dist/lead-queue-runtime.d.ts +1 -1
- package/dist/lead-queue-runtime.js +1 -1
- package/dist/lead-route.d.ts +56 -0
- package/dist/lead-route.js +104 -0
- package/dist/lead.d.ts +4 -2
- package/dist/lead.js +79 -26
- package/dist/live-reply.d.ts +83 -0
- package/dist/live-reply.js +302 -0
- package/dist/operate.js +39 -1
- package/dist/project-knowledge.d.ts +3 -3
- package/dist/reply-shape.d.ts +9 -0
- package/dist/reply-shape.js +13 -0
- package/dist/serve.js +11 -4
- package/dist/server/chat.d.ts +2 -1
- package/dist/server/chat.js +18 -6
- package/dist/server/live.js +18 -21
- package/dist/server/render-chat.d.ts +5 -0
- package/dist/server/render-chat.js +4 -2
- package/dist/server/render-settings.d.ts +8 -0
- package/dist/server/render-settings.js +13 -2
- package/dist/server/route-table.js +2 -0
- package/dist/server/settings.js +14 -1
- package/dist/slack-api.js +11 -0
- package/dist/slack-chat.d.ts +11 -1
- package/dist/slack-chat.js +50 -5
- package/dist/slack-settings.js +8 -1
- package/dist/slack.js +10 -3
- package/dist/store.d.ts +31 -2
- package/dist/store.js +59 -6
- package/dist/subscription-chat.d.ts +2 -0
- package/dist/subscription-chat.js +3 -1
- package/dist/teams-chat.d.ts +9 -2
- package/dist/teams-chat.js +50 -2
- package/dist/teams.js +5 -2
- package/dist/teardown-git.js +33 -3
- package/dist/telegram-lead.d.ts +3 -0
- package/dist/telegram-lead.js +63 -9
- package/dist/telegram-live.d.ts +116 -0
- package/dist/telegram-live.js +224 -0
- package/dist/telegram.d.ts +2 -0
- package/dist/telegram.js +50 -2
- package/dist/toolroll-update.js +4 -1
- package/package.json +1 -1
package/dist/lead.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type LeadProgress } from "./lead-progress.js";
|
|
2
2
|
import type { ChatConfig, LeadSession, LeadThread, Store } from "./store.js";
|
|
3
|
+
import type { LeadFailureDetail } from "./lead-latency-schema.js";
|
|
3
4
|
import type { Integration } from "./integrations.js";
|
|
4
5
|
import type { VerifiedApprover } from "./principal.js";
|
|
5
6
|
import { type LeadHistoryMessage } from "./converse.js";
|
|
@@ -128,8 +129,9 @@ export declare const LEAD_FAILURE_COPY: {
|
|
|
128
129
|
readonly empty: "The lead sent back an empty answer. Nothing was kept. Send your message again.";
|
|
129
130
|
readonly unusable: "The lead's answer couldn't be used, so it was thrown away. Nothing was kept. Send your message again.";
|
|
130
131
|
};
|
|
131
|
-
/** A failed turn's saved reason as plain words, for a surface that reads the turn back later (a restart, a delivery retry).
|
|
132
|
-
|
|
132
|
+
/** A failed turn's saved reason as plain words, for a surface that reads the turn back later (a restart, a delivery retry).
|
|
133
|
+
* Its saved detail (v158), when known, names the exact cause the person saw live ("not installed", "sign-in expired"). */
|
|
134
|
+
export declare function leadFailureText(reason: string | null, unknownSpend?: boolean, detail?: LeadFailureDetail | null): string;
|
|
133
135
|
/** How long a provider that ignored its abort at the turn's deadline is waited for before the turn ends without it.
|
|
134
136
|
* Its process group was already killed at the deadline; this only bounds the wait for the transport to say so. */
|
|
135
137
|
export declare const LEAD_ABORT_GRACE_MS = 5000;
|
package/dist/lead.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { leadToolFailureReason, leadToolLabel } from "./lead-progress.js";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { leadTimeoutNotice } from "./store.js";
|
|
4
|
+
import { leadAskAwaitsAnswer, leadQuickAnswersOn, leadTimeoutNotice, setLeadTurnRoute } from "./store.js";
|
|
5
|
+
import { quickAdvertises, quickLeadModel, quickRoute, quickStepEscalates, routeWords } from "./lead-route.js";
|
|
5
6
|
import { RESULT_IMAGES_PER_TURN_CAP } from "./chat-evidence.js";
|
|
6
7
|
import { isVerifiedApprover, reproveApprover } from "./principal.js";
|
|
7
8
|
import { LEAD_MAX_CALLS_PER_STEP, LEAD_MAX_STEPS, LEAD_TOOL_RESULT_CAP_BYTES, TURN_WALL_CLOCK_MS, composeLeadRequest, credentialKeyOf, isDirectChatProvider, leadWorstCaseForPrice, performLeadRequest, priceForConfig, cacheWorstPrice, settleWithCache, subscriptionCredentialKey, } from "./converse.js";
|
|
@@ -74,8 +75,9 @@ export const LEAD_FAILURE_COPY = {
|
|
|
74
75
|
empty: "The lead sent back an empty answer. Nothing was kept. Send your message again.",
|
|
75
76
|
unusable: "The lead's answer couldn't be used, so it was thrown away. Nothing was kept. Send your message again.",
|
|
76
77
|
};
|
|
77
|
-
/** A failed turn's saved reason as plain words, for a surface that reads the turn back later (a restart, a delivery retry).
|
|
78
|
-
|
|
78
|
+
/** A failed turn's saved reason as plain words, for a surface that reads the turn back later (a restart, a delivery retry).
|
|
79
|
+
* Its saved detail (v158), when known, names the exact cause the person saw live ("not installed", "sign-in expired"). */
|
|
80
|
+
export function leadFailureText(reason, unknownSpend = false, detail = null) {
|
|
79
81
|
// A turn whose cost is unknown paused chat: say what happened (its reason's first sentence), then that the person must
|
|
80
82
|
// confirm the cost before chat runs again — never "send it again", which would only meet the pause.
|
|
81
83
|
if (unknownSpend) {
|
|
@@ -87,6 +89,10 @@ export function leadFailureText(reason, unknownSpend = false) {
|
|
|
87
89
|
const happened = /^[^.]*\./.exec(leadFailureText(reason))?.[0] ?? "The lead's reply didn't finish.";
|
|
88
90
|
return `${happened} Its cost isn't known, so chat is paused. Confirm the cost on the Chat page to turn chat back on.`;
|
|
89
91
|
}
|
|
92
|
+
if (reason === "provider-error" && detail === "not-installed")
|
|
93
|
+
return LEAD_FAILURE_COPY.notInstalled;
|
|
94
|
+
if (reason === "provider-error" && detail === "sign-in")
|
|
95
|
+
return LEAD_FAILURE_COPY.signIn;
|
|
90
96
|
switch (reason) {
|
|
91
97
|
case "over-reservation": return "This reply reported more use than was set aside for it, so it was stopped and its cost counted. Send your message again.";
|
|
92
98
|
case "timeout":
|
|
@@ -212,6 +218,13 @@ export async function runLeadTurn(input) {
|
|
|
212
218
|
// The channel's standing, proved before anything is admitted or sent.
|
|
213
219
|
if (input.revalidate !== undefined && !(await input.revalidate()).ok)
|
|
214
220
|
return refuse("channel");
|
|
221
|
+
// Which model answers (responsive-chat.md §8), decided once, before the turn: the faster one only for a plain read
|
|
222
|
+
// question with quick answers on; everything else on the configured model, as before.
|
|
223
|
+
const quickOn = leadQuickAnswersOn(store);
|
|
224
|
+
const quickModel = quickOn ? quickLeadModel(store) : config.model;
|
|
225
|
+
let route = quickRoute({ enabled: quickOn, provider: config.provider, mainModel: config.model, message,
|
|
226
|
+
hasContext: input.context !== undefined, hasReplyImage: input.replyImage !== undefined, askOpen: quickOn && leadAskAwaitsAnswer(store, thread.id, clock()),
|
|
227
|
+
quickModel, quickAllowed: quickOn && store.agentPolicyRefusal(config.provider, quickModel) === null }).route;
|
|
215
228
|
let now = clock();
|
|
216
229
|
store.sweepStaleLeadTurns(now);
|
|
217
230
|
const view = leadViewContextFor(store, who);
|
|
@@ -282,6 +295,7 @@ export async function runLeadTurn(input) {
|
|
|
282
295
|
const started = store.startLeadTurn(turnId, now);
|
|
283
296
|
if (!started.ok)
|
|
284
297
|
throw new Error("new lead turn could not start");
|
|
298
|
+
setLeadTurnRoute(store, turnId, started.generation, route);
|
|
285
299
|
if (input.queuedMessageId === undefined)
|
|
286
300
|
store.appendLeadMessage({ thread: thread.id, turn: turnId, role: "operator", text: message }, now);
|
|
287
301
|
else {
|
|
@@ -308,6 +322,12 @@ export async function runLeadTurn(input) {
|
|
|
308
322
|
}
|
|
309
323
|
catch { /* a watcher never breaks the turn */ } };
|
|
310
324
|
progress({ kind: "started", turn: turnId });
|
|
325
|
+
/** Text that looked like a secret was held back: every preview closes, once (responsive-chat.md §2.1). */
|
|
326
|
+
let blockedSaid = false;
|
|
327
|
+
const blockPreview = () => { if (!blockedSaid) {
|
|
328
|
+
blockedSaid = true;
|
|
329
|
+
progress({ kind: "blocked", turn: turnId });
|
|
330
|
+
} };
|
|
311
331
|
let proposals = 0;
|
|
312
332
|
let reads = 0;
|
|
313
333
|
let steps = 0;
|
|
@@ -336,18 +356,18 @@ export async function runLeadTurn(input) {
|
|
|
336
356
|
return store.listLeadTurnEvidence(turnId).map(one => one.artifact);
|
|
337
357
|
};
|
|
338
358
|
/** The turn ends failed: its drafts are deleted, its cost settled — the whole reservation when any of it is unknown. */
|
|
339
|
-
const fail = (failed, message, unknownSpend) => {
|
|
359
|
+
const fail = (failed, message, unknownSpend, failureDetail = null) => {
|
|
340
360
|
now = clock();
|
|
341
|
-
store.finalizeLeadTurn(turnId, started.generation, { state: "failed", settledMicrousd: settled, unknownSpend, tokensIn, tokensOut, failureReason: failed }, now);
|
|
361
|
+
store.finalizeLeadTurn(turnId, started.generation, { state: "failed", settledMicrousd: settled, unknownSpend, tokensIn, tokensOut, failureReason: failed, failureDetail }, now);
|
|
342
362
|
store.dropLeadAsk(turnId);
|
|
343
363
|
return { ok: false, turn: turnId, failed, message, unknownSpend };
|
|
344
364
|
};
|
|
345
365
|
/** The turn stopped at its deadline: the notice is saved in the thread with whatever its completed tool calls proposed, in
|
|
346
366
|
* the same write that ends the turn, so a refresh, another device or a restart shows the same outcome. */
|
|
347
|
-
const timedOut = (unknownSpend) => {
|
|
367
|
+
const timedOut = (unknownSpend, failureDetail = null) => {
|
|
348
368
|
now = clock();
|
|
349
369
|
const message = leadTimeoutNotice(proposals > 0, unknownSpend);
|
|
350
|
-
const saved = store.finalizeLeadTurn(turnId, started.generation, { state: "failed", settledMicrousd: settled, unknownSpend, tokensIn, tokensOut, failureReason: "timeout", keepProposals: true, message: { text: message, activity: activitySummary(reads, proposals, steps) } }, now);
|
|
370
|
+
const saved = store.finalizeLeadTurn(turnId, started.generation, { state: "failed", settledMicrousd: settled, unknownSpend, tokensIn, tokensOut, failureReason: "timeout", failureDetail, keepProposals: true, message: { text: message, activity: activitySummary(reads, proposals, steps) } }, now);
|
|
351
371
|
store.dropLeadAsk(turnId);
|
|
352
372
|
if (!saved)
|
|
353
373
|
return { ok: false, turn: turnId, failed: "superseded", message: LEAD_FAILURE_COPY.stopped, unknownSpend: false };
|
|
@@ -387,6 +407,8 @@ export async function runLeadTurn(input) {
|
|
|
387
407
|
let repaired = false;
|
|
388
408
|
/** The turn's one shorten step for a reply over its channel's message limit (LEAD_REPLY_LIMITS); still over, it is split. */
|
|
389
409
|
let shortened = false;
|
|
410
|
+
/** Why the next step runs, when it re-asks about the reply itself (repair or shorten): the live status says so. */
|
|
411
|
+
let nextReason;
|
|
390
412
|
const replyLimit = input.channel === undefined ? null : LEAD_REPLY_LIMITS[input.channel];
|
|
391
413
|
const unbacked = (text) => {
|
|
392
414
|
const claim = deliverableClaim(text);
|
|
@@ -403,7 +425,10 @@ export async function runLeadTurn(input) {
|
|
|
403
425
|
if (remainingMs <= 0)
|
|
404
426
|
return timedOut(false);
|
|
405
427
|
// This request's advertised tools: fixed for the request; a tool found in its reply is advertised from the next one.
|
|
406
|
-
|
|
428
|
+
// A quick step (§8) advertises the reads, and find_tools as its way to ask for more, on the faster model.
|
|
429
|
+
const quick = route === "fast";
|
|
430
|
+
const stepModel = quick ? quickModel : config.model;
|
|
431
|
+
const advertised = quick ? LEAD_TOOL_SCHEMAS.filter(one => quickAdvertises(one.name)) : advertisedTools();
|
|
407
432
|
const advertisedNames = new Set(advertised.map(one => one.name));
|
|
408
433
|
const request = direct ? composeDirect(input.key, advertised) : composeSubscription(advertised);
|
|
409
434
|
// Tool results join the outbound body: scanned again before every dispatch.
|
|
@@ -411,7 +436,7 @@ export async function runLeadTurn(input) {
|
|
|
411
436
|
if (scanForSecrets(outbound).length > 0) {
|
|
412
437
|
return fail("secret-refused", LEAD_FAILURE_COPY.secretRead, false);
|
|
413
438
|
}
|
|
414
|
-
const step = store.openLeadStep({ leadTurn: turnId, generation: started.generation, approver: who.name, credentialKey, provider: config.provider, model:
|
|
439
|
+
const step = store.openLeadStep({ leadTurn: turnId, generation: started.generation, approver: who.name, credentialKey, provider: config.provider, model: stepModel, deadlineMs: remainingMs + 10_000 }, now);
|
|
415
440
|
if (!step.ok) {
|
|
416
441
|
if (step.reason === "latched")
|
|
417
442
|
return fail("latched", LEAD_FAILURE_COPY.paused, false);
|
|
@@ -421,7 +446,8 @@ export async function runLeadTurn(input) {
|
|
|
421
446
|
if (!stepStarted.ok)
|
|
422
447
|
return fail("provider-error", LEAD_FAILURE_COPY.notStarted, false);
|
|
423
448
|
steps++;
|
|
424
|
-
progress({ kind: "step", turn: turnId, step: steps });
|
|
449
|
+
progress({ kind: "step", turn: turnId, step: steps, ...(nextReason === undefined ? {} : { reason: nextReason }) });
|
|
450
|
+
nextReason = undefined;
|
|
425
451
|
const requestBytes = Buffer.byteLength(outbound, "utf8");
|
|
426
452
|
let result;
|
|
427
453
|
// The turn owns its deadline (lead-stall, release check 2438): at the deadline the request is aborted, which ends a
|
|
@@ -430,7 +456,9 @@ export async function runLeadTurn(input) {
|
|
|
430
456
|
const controller = new AbortController();
|
|
431
457
|
const abortAt = setTimeout(() => controller.abort(), remainingMs);
|
|
432
458
|
const settle = (work) => settleBy(work, remainingMs + LEAD_ABORT_GRACE_MS)
|
|
433
|
-
|
|
459
|
+
// A harness that ended at the deadline still says whether it had written anything (recorded, never shown).
|
|
460
|
+
.then(value => value === LATE ? { ok: false, problem: "timeout" } : controller.signal.aborted
|
|
461
|
+
? { ok: false, problem: "timeout", ...(!value.ok && "quiet" in value && typeof value.quiet === "boolean" ? { quiet: value.quiet } : {}) } : value)
|
|
434
462
|
.finally(() => clearTimeout(abortAt));
|
|
435
463
|
if (direct) {
|
|
436
464
|
result = await settle(performLeadRequest(request, directProvider, controller.signal, fetcher)
|
|
@@ -440,7 +468,7 @@ export async function runLeadTurn(input) {
|
|
|
440
468
|
const runner = input.subscriptionRunner ?? performSubscriptionLeadRequest;
|
|
441
469
|
result = await settle((async () => runner({
|
|
442
470
|
provider: subscriptionProvider,
|
|
443
|
-
model:
|
|
471
|
+
model: stepModel,
|
|
444
472
|
system: LEAD_CONTRACT,
|
|
445
473
|
dataDocument: document,
|
|
446
474
|
history,
|
|
@@ -449,15 +477,22 @@ export async function runLeadTurn(input) {
|
|
|
449
477
|
signal: controller.signal,
|
|
450
478
|
// The reply as it is written; text that looks like a secret is
|
|
451
479
|
// never shown (the finished reply is scanned again before saving).
|
|
452
|
-
...(input.onProgress === undefined ? {} : { onText: (text) => { if (
|
|
453
|
-
|
|
480
|
+
...(input.onProgress === undefined ? {} : { onText: (text) => { if (controller.signal.aborted)
|
|
481
|
+
return; if (scanForSecrets(text).length === 0)
|
|
482
|
+
progress({ kind: "text", turn: turnId, step: steps, text });
|
|
483
|
+
else
|
|
484
|
+
blockPreview(); } }),
|
|
454
485
|
}))().catch(() => ({ ok: false, problem: "provider-error" })));
|
|
455
486
|
}
|
|
456
487
|
now = clock();
|
|
457
488
|
const finishStep = (outcome) => store.finalizeChatTurn(step.id, stepStarted.generation, outcome, now);
|
|
458
489
|
if (!result.ok) {
|
|
459
490
|
if (!direct) {
|
|
460
|
-
|
|
491
|
+
// The exact cause, saved (v158): what a later reader shows, and what the next timeout diagnosis reads.
|
|
492
|
+
const quiet = "quiet" in result ? result.quiet : undefined;
|
|
493
|
+
const detail = result.problem === "timeout" ? (quiet === true ? "timeout-silent" : quiet === false ? "timeout-output" : null)
|
|
494
|
+
: result.problem === "not-found" ? "not-installed" : result.problem === "malformed-reply" ? null : "sign-in";
|
|
495
|
+
finishStep({ state: "failed", failureReason: result.problem === "timeout" ? "timeout" : result.problem === "malformed-reply" ? "malformed-reply" : "provider-error", settledMicrousd: 0, failureDetail: detail });
|
|
461
496
|
const message = result.problem === "not-found"
|
|
462
497
|
? LEAD_FAILURE_COPY.notInstalled
|
|
463
498
|
: result.problem === "timeout"
|
|
@@ -466,29 +501,32 @@ export async function runLeadTurn(input) {
|
|
|
466
501
|
? LEAD_FAILURE_COPY.unreadable
|
|
467
502
|
: LEAD_FAILURE_COPY.signIn;
|
|
468
503
|
if (result.problem === "timeout")
|
|
469
|
-
return timedOut(false);
|
|
470
|
-
return fail(result.problem === "malformed-reply" ? "malformed-reply" : "provider-error", message, false);
|
|
504
|
+
return timedOut(false, detail);
|
|
505
|
+
return fail(result.problem === "malformed-reply" ? "malformed-reply" : "provider-error", message, false, detail);
|
|
471
506
|
}
|
|
472
507
|
if (result.problem.startsWith("status-")) {
|
|
473
508
|
// The provider ANSWERED with an error: nothing billed for this step.
|
|
474
|
-
finishStep({ state: "failed", failureReason: "provider-error", settledMicrousd: 0 });
|
|
475
|
-
return fail("provider-error", LEAD_FAILURE_COPY.refused, false);
|
|
509
|
+
finishStep({ state: "failed", failureReason: "provider-error", settledMicrousd: 0, failureDetail: "refused" });
|
|
510
|
+
return fail("provider-error", LEAD_FAILURE_COPY.refused, false, "refused");
|
|
476
511
|
}
|
|
477
512
|
if (result.problem === "timeout") {
|
|
478
513
|
finishStep({ state: "failed", failureReason: "timeout", settledMicrousd: null, unknownSpend: true });
|
|
479
514
|
return timedOut(true);
|
|
480
515
|
}
|
|
481
516
|
if (result.problem === "network") {
|
|
482
|
-
finishStep({ state: "failed", failureReason: "provider-error", settledMicrousd: null, unknownSpend: true });
|
|
483
|
-
return fail("provider-error", LEAD_FAILURE_COPY.lostUnknownCost, true);
|
|
517
|
+
finishStep({ state: "failed", failureReason: "provider-error", settledMicrousd: null, unknownSpend: true, failureDetail: "lost" });
|
|
518
|
+
return fail("provider-error", LEAD_FAILURE_COPY.lostUnknownCost, true, "lost");
|
|
484
519
|
}
|
|
485
520
|
finishStep({ state: "failed", failureReason: "malformed-reply", settledMicrousd: null, unknownSpend: true });
|
|
486
521
|
return fail("malformed-reply", LEAD_FAILURE_COPY.unreadableUnknownCost, true);
|
|
487
522
|
}
|
|
488
523
|
const answer = result.answer;
|
|
489
524
|
// The streamed reply's finished text, once, secret-checked whole: only now may its last word show live.
|
|
490
|
-
if (!direct && input.onProgress !== undefined && !controller.signal.aborted && answer.text !== ""
|
|
491
|
-
|
|
525
|
+
if (!direct && input.onProgress !== undefined && !controller.signal.aborted && answer.text !== "") {
|
|
526
|
+
if (scanForSecrets(answer.text).length === 0)
|
|
527
|
+
progress({ kind: "text", turn: turnId, step: steps, text: answer.text, final: true });
|
|
528
|
+
else
|
|
529
|
+
blockPreview();
|
|
492
530
|
}
|
|
493
531
|
// Usage that cannot be true — more input tokens than bytes sent — is a
|
|
494
532
|
// malformed reply with unknown cost, never a number to settle by.
|
|
@@ -512,7 +550,9 @@ export async function runLeadTurn(input) {
|
|
|
512
550
|
cacheReadTokens: answer.cacheReadTokens ?? null, cacheWriteTokens: answer.cacheWriteTokens ?? null,
|
|
513
551
|
// v140: what the provider itself reported for this request (absent: unreported, kept NULL).
|
|
514
552
|
modelReported: answer.modelReported ?? null, providerTurns: answer.providerTurns ?? null, reportedCostUsd: answer.reportedCostUsd ?? null,
|
|
515
|
-
billingReported: answer.billingReported ?? null
|
|
553
|
+
billingReported: answer.billingReported ?? null,
|
|
554
|
+
// v158: the tools this step asked for, names only (responsive-chat.md §7.2), so the next latency call is data-driven.
|
|
555
|
+
tools: answer.calls.map(one => one.name) });
|
|
516
556
|
tokensIn += answer.tokensIn;
|
|
517
557
|
tokensOut += answer.tokensOut;
|
|
518
558
|
if (answer.cacheReadTokens === undefined)
|
|
@@ -527,6 +567,14 @@ export async function runLeadTurn(input) {
|
|
|
527
567
|
const changed = guard(await revalidated());
|
|
528
568
|
if (changed !== null)
|
|
529
569
|
return changed;
|
|
570
|
+
// A quick step that asked for more than a read (§8): its calls are discarded unrun, and the configured model takes the
|
|
571
|
+
// reply over from the same history, for this step and every later one. Steps and the turn's deadline still bind.
|
|
572
|
+
if (route === "fast" && quickStepEscalates(answer.calls)) {
|
|
573
|
+
route = "escalated";
|
|
574
|
+
setLeadTurnRoute(store, turnId, started.generation, route);
|
|
575
|
+
lastText = "";
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
530
578
|
if (answer.calls.length === 0) {
|
|
531
579
|
if (answer.text.trim() === "")
|
|
532
580
|
return fail("malformed-reply", LEAD_FAILURE_COPY.empty, false);
|
|
@@ -534,6 +582,7 @@ export async function runLeadTurn(input) {
|
|
|
534
582
|
const claim = unbacked(answer.text);
|
|
535
583
|
if (claim !== null && !repaired && steps < maxSteps) {
|
|
536
584
|
repaired = true;
|
|
585
|
+
nextReason = "repair";
|
|
537
586
|
history.push({ role: "assistant", text: answer.text, calls: [] });
|
|
538
587
|
history.push({ role: "operator", text: deliverableRepair(claim) });
|
|
539
588
|
continue;
|
|
@@ -541,6 +590,7 @@ export async function runLeadTurn(input) {
|
|
|
541
590
|
const candidate = claim === null ? answer.text : dropDeliverableClaims(answer.text);
|
|
542
591
|
if (replyLimit !== null && candidate.length > replyLimit && !shortened && steps < maxSteps) {
|
|
543
592
|
shortened = true;
|
|
593
|
+
nextReason = "shorten";
|
|
544
594
|
history.push({ role: "assistant", text: answer.text, calls: [] });
|
|
545
595
|
history.push({ role: "operator", text: shortenAsk([{ field: "Your reply", limit: replyLimit, length: candidate.length }]) });
|
|
546
596
|
continue;
|
|
@@ -598,10 +648,13 @@ export async function runLeadTurn(input) {
|
|
|
598
648
|
reply = `${last === "" ? "" : `${last}\n\n`}(I stopped here: this answer needed more steps than ${maxSteps < LEAD_MAX_STEPS ? "this conversation's remaining spending allows" : "one reply allows"}. Ask me to carry on.)`;
|
|
599
649
|
}
|
|
600
650
|
// Ruling 11: model text is scanned before it becomes durable.
|
|
601
|
-
if (scanForSecrets(reply).length > 0)
|
|
651
|
+
if (scanForSecrets(reply).length > 0) {
|
|
652
|
+
blockPreview();
|
|
602
653
|
return fail("secret-refused", LEAD_FAILURE_COPY.secretReply, false);
|
|
654
|
+
}
|
|
603
655
|
now = clock();
|
|
604
|
-
const
|
|
656
|
+
const routed = routeWords(route, quickModel, config.model);
|
|
657
|
+
const activity = `${activitySummary(reads, proposals, steps, cacheKnown && tokensIn > 0 ? Math.min(1, cacheRead / tokensIn) : null)}${routed === null ? "" : ` · ${routed}`}`;
|
|
605
658
|
// One write (finding 12): settle, debit, promote the drafts, append the assistant text.
|
|
606
659
|
const finalized = store.finalizeLeadTurn(turnId, started.generation, { state: "answered", settledMicrousd: settled, tokensIn, tokensOut, message: { text: reply, activity } }, now);
|
|
607
660
|
if (!finalized)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { LeadLiveTool, LeadProgress } from "./lead-progress.js";
|
|
2
|
+
import { type ShapeOptions } from "./reply-shape.js";
|
|
3
|
+
export type LiveView = {
|
|
4
|
+
phase: "queued";
|
|
5
|
+
} | {
|
|
6
|
+
phase: "working";
|
|
7
|
+
status: string;
|
|
8
|
+
} | {
|
|
9
|
+
phase: "writing";
|
|
10
|
+
status: string;
|
|
11
|
+
text: string;
|
|
12
|
+
} | {
|
|
13
|
+
phase: "closed";
|
|
14
|
+
why: "answered" | "failed" | "blocked";
|
|
15
|
+
};
|
|
16
|
+
/** The status line's fixed words (§2.3). A running tool reads as its own label plus "…". */
|
|
17
|
+
export declare const LIVE_STATUS: {
|
|
18
|
+
readonly queued: "Waiting for your earlier messages…";
|
|
19
|
+
readonly thinking: "Thinking…";
|
|
20
|
+
readonly writing: "Writing…";
|
|
21
|
+
readonly repair: "Fixing the reply…";
|
|
22
|
+
readonly shorten: "Shortening the reply…";
|
|
23
|
+
/** A preview closed because text looked like a password or key; the turn itself may still answer. */
|
|
24
|
+
readonly held: "Working on it…";
|
|
25
|
+
};
|
|
26
|
+
/** Could the end of this text be the start of a password or key? Then nothing of it shows yet. */
|
|
27
|
+
export declare function secretMayStart(text: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* What of a reply still being written is settled: everything before its last whitespace (the unfinished last word
|
|
30
|
+
* stays out), then cut back before any code fence, code span, bold run or link that hasn't closed yet. Trailing
|
|
31
|
+
* whitespace is trimmed.
|
|
32
|
+
*/
|
|
33
|
+
export declare function settledText(raw: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* A partial as it may be shown: `blocked` when it holds a password or key (the preview closes), `withheld` while its
|
|
36
|
+
* tail could still become one (show nothing new), else the settled text shaped for display. A step's finished text
|
|
37
|
+
* (`final`, secret-checked whole by the engine) shows whole.
|
|
38
|
+
*/
|
|
39
|
+
export declare function partialDisplay(raw: string, final: boolean, shape?: ShapeOptions): {
|
|
40
|
+
kind: "blocked";
|
|
41
|
+
} | {
|
|
42
|
+
kind: "withheld";
|
|
43
|
+
} | {
|
|
44
|
+
kind: "shown";
|
|
45
|
+
text: string;
|
|
46
|
+
};
|
|
47
|
+
export type LiveReply = {
|
|
48
|
+
/** Feed one engine event; true when the view changed. Events after close are ignored. */
|
|
49
|
+
push(event: LeadProgress): boolean;
|
|
50
|
+
/** The turn ended (its outcome is the engine's, never inferred from text). Idempotent; a blocked close stays blocked. */
|
|
51
|
+
close(why: "answered" | "failed"): void;
|
|
52
|
+
view(): LiveView;
|
|
53
|
+
/** The status line now, also once a blocked preview closed while the turn runs on. */
|
|
54
|
+
status(): string;
|
|
55
|
+
/** Each step's tool calls, for a surface that lists them behind a disclosure. Fixed copy only, never model text. */
|
|
56
|
+
tools(): LeadLiveTool[][];
|
|
57
|
+
readonly turn: number | null;
|
|
58
|
+
};
|
|
59
|
+
/** One turn's live reply. `shape`: the owner's message (`asked`) and this app's origins, as the final reply is shaped. */
|
|
60
|
+
export declare function createLiveReply(shape?: ShapeOptions, options?: {
|
|
61
|
+
queued?: boolean;
|
|
62
|
+
}): LiveReply;
|
|
63
|
+
export type LiveSender<V> = {
|
|
64
|
+
/** Offer the newest view: it replaces any view still waiting. Ignored once closed. */
|
|
65
|
+
offer(view: V): void;
|
|
66
|
+
/** Drop the waiting view, wait for the request in flight to settle, then refuse every later view. */
|
|
67
|
+
close(): Promise<void>;
|
|
68
|
+
/** Grows with every offered view; a transport may use it to refuse an older one. */
|
|
69
|
+
readonly generation: number;
|
|
70
|
+
readonly closed: boolean;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* The serialized sender for one live surface message (§2.4). `standing` re-proves the viewer's right to see this turn
|
|
74
|
+
* before every send (it may await; the close is re-checked after it). `spacingMs` is the least time between two sends,
|
|
75
|
+
* read before each one (a surface widens it after the platform asks for a pause). A failed send drops that view:
|
|
76
|
+
* previews are best effort.
|
|
77
|
+
*/
|
|
78
|
+
export declare function createLiveSender<V>(options: {
|
|
79
|
+
send: (view: V, generation: number) => unknown;
|
|
80
|
+
standing?: () => boolean | Promise<boolean>;
|
|
81
|
+
spacingMs?: number | (() => number);
|
|
82
|
+
now?: () => number;
|
|
83
|
+
}): LiveSender<V>;
|