toolroll 0.9.96 → 0.9.98
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 +18 -0
- package/dist/approval-authority.d.ts +10 -1
- package/dist/approval-authority.js +18 -2
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +32 -32
- package/dist/browser-workspace.d.ts +6 -3
- package/dist/chat-actions.d.ts +40 -5
- package/dist/chat-actions.js +124 -13
- package/dist/chat-card.js +4 -1
- package/dist/chat-channel.d.ts +3 -0
- package/dist/chat-channel.js +24 -7
- package/dist/chat-decide.js +4 -2
- 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/chat-task-actions.js +1 -1
- package/dist/chat-terms.d.ts +8 -0
- package/dist/chat-terms.js +14 -0
- package/dist/chat-voice.js +1 -1
- package/dist/contracts/chat-actions.d.ts +6 -0
- package/dist/contracts/chat-actions.js +2 -0
- package/dist/contracts/cli.d.ts +6 -0
- package/dist/contracts/cli.js +3 -0
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +1 -0
- package/dist/contracts/lead-tools.d.ts +17 -0
- package/dist/contracts/lead-tools.js +4 -1
- package/dist/contracts/store-json.d.ts +1 -0
- 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/fast-start.d.ts +83 -6
- package/dist/fast-start.js +112 -15
- package/dist/home-groups.d.ts +39 -0
- package/dist/home-groups.js +33 -0
- package/dist/lead-answer-eval.d.ts +93 -0
- package/dist/lead-answer-eval.js +259 -0
- package/dist/lead-capabilities.js +1 -1
- 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-context.js +2 -2
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +10 -7
- package/dist/lead-doors.d.ts +32 -0
- package/dist/lead-doors.js +80 -14
- 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-status.d.ts +3 -0
- package/dist/lead-status.js +8 -3
- package/dist/lead-tools.d.ts +20 -0
- package/dist/lead-tools.js +66 -3
- package/dist/lead-voice.js +1 -1
- package/dist/lead.d.ts +4 -2
- package/dist/lead.js +97 -28
- package/dist/live-reply.d.ts +83 -0
- package/dist/live-reply.js +302 -0
- package/dist/local-owner.js +2 -0
- package/dist/one-step-chat-schema.d.ts +22 -0
- package/dist/one-step-chat-schema.js +37 -0
- package/dist/operate.d.ts +27 -1
- package/dist/operate.js +245 -33
- package/dist/project-delete.js +9 -0
- package/dist/project-knowledge.d.ts +10 -10
- package/dist/publish.d.ts +2 -0
- package/dist/publish.js +1 -1
- package/dist/pull-request-flow.d.ts +4 -0
- package/dist/pull-request-flow.js +16 -1
- package/dist/reply-shape.d.ts +9 -0
- package/dist/reply-shape.js +13 -0
- package/dist/reply-voice.d.ts +23 -0
- package/dist/reply-voice.js +37 -0
- package/dist/scope.d.ts +6 -1
- package/dist/scope.js +3 -2
- package/dist/serve.js +11 -4
- package/dist/server/chat.d.ts +2 -1
- package/dist/server/chat.js +22 -10
- package/dist/server/chrome.js +2 -2
- package/dist/server/home.d.ts +4 -3
- package/dist/server/home.js +33 -28
- package/dist/server/live.js +18 -21
- package/dist/server/render-chat.d.ts +5 -0
- package/dist/server/render-chat.js +15 -8
- package/dist/server/render-pages.js +2 -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/setup-guide.d.ts +2 -2
- 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 +33 -2
- package/dist/store.js +92 -16
- 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/telegram-lead.d.ts +3 -0
- package/dist/telegram-lead.js +132 -14
- package/dist/telegram-live.d.ts +116 -0
- package/dist/telegram-live.js +224 -0
- package/dist/telegram-status.js +9 -4
- package/dist/telegram.d.ts +12 -0
- package/dist/telegram.js +142 -10
- package/dist/toolroll-update.js +6 -1
- package/dist/tui-contract.d.ts +2 -0
- package/dist/tui-source.js +3 -0
- package/dist/tui.js +41 -41
- package/dist/typed-start.d.ts +51 -0
- package/dist/typed-start.js +98 -0
- package/dist/wake-hub.d.ts +72 -0
- package/dist/wake-hub.js +151 -0
- package/package.json +1 -1
package/dist/lead.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
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";
|
|
8
9
|
import { redactSecretLines, scanForSecrets } from "./evidence.js";
|
|
9
10
|
import { redactSecretAssignments } from "./builder.js";
|
|
10
11
|
import { LEAD_CONTRACT } from "./lead-contract.js";
|
|
11
|
-
import { LEAD_CORE_TOOLS, LEAD_MAX_PROPOSALS_PER_TURN, LEAD_TOOL_SCHEMAS, executeLeadTool, isLeadTool, leadViewContextFor, projectLabelForLead, redactForLead, toolResultBytes } from "./lead-tools.js";
|
|
12
|
+
import { LEAD_CORE_TOOLS, LEAD_MAX_PROPOSALS_PER_TURN, LEAD_TOOL_SCHEMAS, executeLeadTool, isLeadTool, leadViewContextFor, projectLabelForLead, redactForLead, startInputOf, toolResultBytes } from "./lead-tools.js";
|
|
13
|
+
import { prepareChatStart } from "./fast-start.js";
|
|
12
14
|
import { composeSubscriptionLeadPrompt, performSubscriptionLeadRequest } from "./subscription-chat.js";
|
|
13
15
|
import { LEAD_REPLY_LIMITS, leadContext } from './lead-context.js';
|
|
14
16
|
import { shortenAsk } from './text-limits.js';
|
|
@@ -74,8 +76,9 @@ export const LEAD_FAILURE_COPY = {
|
|
|
74
76
|
empty: "The lead sent back an empty answer. Nothing was kept. Send your message again.",
|
|
75
77
|
unusable: "The lead's answer couldn't be used, so it was thrown away. Nothing was kept. Send your message again.",
|
|
76
78
|
};
|
|
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
|
-
|
|
79
|
+
/** A failed turn's saved reason as plain words, for a surface that reads the turn back later (a restart, a delivery retry).
|
|
80
|
+
* Its saved detail (v158), when known, names the exact cause the person saw live ("not installed", "sign-in expired"). */
|
|
81
|
+
export function leadFailureText(reason, unknownSpend = false, detail = null) {
|
|
79
82
|
// A turn whose cost is unknown paused chat: say what happened (its reason's first sentence), then that the person must
|
|
80
83
|
// confirm the cost before chat runs again — never "send it again", which would only meet the pause.
|
|
81
84
|
if (unknownSpend) {
|
|
@@ -87,6 +90,10 @@ export function leadFailureText(reason, unknownSpend = false) {
|
|
|
87
90
|
const happened = /^[^.]*\./.exec(leadFailureText(reason))?.[0] ?? "The lead's reply didn't finish.";
|
|
88
91
|
return `${happened} Its cost isn't known, so chat is paused. Confirm the cost on the Chat page to turn chat back on.`;
|
|
89
92
|
}
|
|
93
|
+
if (reason === "provider-error" && detail === "not-installed")
|
|
94
|
+
return LEAD_FAILURE_COPY.notInstalled;
|
|
95
|
+
if (reason === "provider-error" && detail === "sign-in")
|
|
96
|
+
return LEAD_FAILURE_COPY.signIn;
|
|
90
97
|
switch (reason) {
|
|
91
98
|
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
99
|
case "timeout":
|
|
@@ -212,6 +219,13 @@ export async function runLeadTurn(input) {
|
|
|
212
219
|
// The channel's standing, proved before anything is admitted or sent.
|
|
213
220
|
if (input.revalidate !== undefined && !(await input.revalidate()).ok)
|
|
214
221
|
return refuse("channel");
|
|
222
|
+
// Which model answers (responsive-chat.md §8), decided once, before the turn: the faster one only for a plain read
|
|
223
|
+
// question with quick answers on; everything else on the configured model, as before.
|
|
224
|
+
const quickOn = leadQuickAnswersOn(store);
|
|
225
|
+
const quickModel = quickOn ? quickLeadModel(store) : config.model;
|
|
226
|
+
let route = quickRoute({ enabled: quickOn, provider: config.provider, mainModel: config.model, message,
|
|
227
|
+
hasContext: input.context !== undefined, hasReplyImage: input.replyImage !== undefined, askOpen: quickOn && leadAskAwaitsAnswer(store, thread.id, clock()),
|
|
228
|
+
quickModel, quickAllowed: quickOn && store.agentPolicyRefusal(config.provider, quickModel) === null }).route;
|
|
215
229
|
let now = clock();
|
|
216
230
|
store.sweepStaleLeadTurns(now);
|
|
217
231
|
const view = leadViewContextFor(store, who);
|
|
@@ -282,6 +296,7 @@ export async function runLeadTurn(input) {
|
|
|
282
296
|
const started = store.startLeadTurn(turnId, now);
|
|
283
297
|
if (!started.ok)
|
|
284
298
|
throw new Error("new lead turn could not start");
|
|
299
|
+
setLeadTurnRoute(store, turnId, started.generation, route);
|
|
285
300
|
if (input.queuedMessageId === undefined)
|
|
286
301
|
store.appendLeadMessage({ thread: thread.id, turn: turnId, role: "operator", text: message }, now);
|
|
287
302
|
else {
|
|
@@ -308,6 +323,12 @@ export async function runLeadTurn(input) {
|
|
|
308
323
|
}
|
|
309
324
|
catch { /* a watcher never breaks the turn */ } };
|
|
310
325
|
progress({ kind: "started", turn: turnId });
|
|
326
|
+
/** Text that looked like a secret was held back: every preview closes, once (responsive-chat.md §2.1). */
|
|
327
|
+
let blockedSaid = false;
|
|
328
|
+
const blockPreview = () => { if (!blockedSaid) {
|
|
329
|
+
blockedSaid = true;
|
|
330
|
+
progress({ kind: "blocked", turn: turnId });
|
|
331
|
+
} };
|
|
311
332
|
let proposals = 0;
|
|
312
333
|
let reads = 0;
|
|
313
334
|
let steps = 0;
|
|
@@ -336,18 +357,18 @@ export async function runLeadTurn(input) {
|
|
|
336
357
|
return store.listLeadTurnEvidence(turnId).map(one => one.artifact);
|
|
337
358
|
};
|
|
338
359
|
/** 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) => {
|
|
360
|
+
const fail = (failed, message, unknownSpend, failureDetail = null) => {
|
|
340
361
|
now = clock();
|
|
341
|
-
store.finalizeLeadTurn(turnId, started.generation, { state: "failed", settledMicrousd: settled, unknownSpend, tokensIn, tokensOut, failureReason: failed }, now);
|
|
362
|
+
store.finalizeLeadTurn(turnId, started.generation, { state: "failed", settledMicrousd: settled, unknownSpend, tokensIn, tokensOut, failureReason: failed, failureDetail }, now);
|
|
342
363
|
store.dropLeadAsk(turnId);
|
|
343
364
|
return { ok: false, turn: turnId, failed, message, unknownSpend };
|
|
344
365
|
};
|
|
345
366
|
/** The turn stopped at its deadline: the notice is saved in the thread with whatever its completed tool calls proposed, in
|
|
346
367
|
* the same write that ends the turn, so a refresh, another device or a restart shows the same outcome. */
|
|
347
|
-
const timedOut = (unknownSpend) => {
|
|
368
|
+
const timedOut = (unknownSpend, failureDetail = null) => {
|
|
348
369
|
now = clock();
|
|
349
370
|
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);
|
|
371
|
+
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
372
|
store.dropLeadAsk(turnId);
|
|
352
373
|
if (!saved)
|
|
353
374
|
return { ok: false, turn: turnId, failed: "superseded", message: LEAD_FAILURE_COPY.stopped, unknownSpend: false };
|
|
@@ -387,6 +408,8 @@ export async function runLeadTurn(input) {
|
|
|
387
408
|
let repaired = false;
|
|
388
409
|
/** The turn's one shorten step for a reply over its channel's message limit (LEAD_REPLY_LIMITS); still over, it is split. */
|
|
389
410
|
let shortened = false;
|
|
411
|
+
/** Why the next step runs, when it re-asks about the reply itself (repair or shorten): the live status says so. */
|
|
412
|
+
let nextReason;
|
|
390
413
|
const replyLimit = input.channel === undefined ? null : LEAD_REPLY_LIMITS[input.channel];
|
|
391
414
|
const unbacked = (text) => {
|
|
392
415
|
const claim = deliverableClaim(text);
|
|
@@ -403,7 +426,10 @@ export async function runLeadTurn(input) {
|
|
|
403
426
|
if (remainingMs <= 0)
|
|
404
427
|
return timedOut(false);
|
|
405
428
|
// This request's advertised tools: fixed for the request; a tool found in its reply is advertised from the next one.
|
|
406
|
-
|
|
429
|
+
// A quick step (§8) advertises the reads, and find_tools as its way to ask for more, on the faster model.
|
|
430
|
+
const quick = route === "fast";
|
|
431
|
+
const stepModel = quick ? quickModel : config.model;
|
|
432
|
+
const advertised = quick ? LEAD_TOOL_SCHEMAS.filter(one => quickAdvertises(one.name)) : advertisedTools();
|
|
407
433
|
const advertisedNames = new Set(advertised.map(one => one.name));
|
|
408
434
|
const request = direct ? composeDirect(input.key, advertised) : composeSubscription(advertised);
|
|
409
435
|
// Tool results join the outbound body: scanned again before every dispatch.
|
|
@@ -411,7 +437,7 @@ export async function runLeadTurn(input) {
|
|
|
411
437
|
if (scanForSecrets(outbound).length > 0) {
|
|
412
438
|
return fail("secret-refused", LEAD_FAILURE_COPY.secretRead, false);
|
|
413
439
|
}
|
|
414
|
-
const step = store.openLeadStep({ leadTurn: turnId, generation: started.generation, approver: who.name, credentialKey, provider: config.provider, model:
|
|
440
|
+
const step = store.openLeadStep({ leadTurn: turnId, generation: started.generation, approver: who.name, credentialKey, provider: config.provider, model: stepModel, deadlineMs: remainingMs + 10_000 }, now);
|
|
415
441
|
if (!step.ok) {
|
|
416
442
|
if (step.reason === "latched")
|
|
417
443
|
return fail("latched", LEAD_FAILURE_COPY.paused, false);
|
|
@@ -421,7 +447,8 @@ export async function runLeadTurn(input) {
|
|
|
421
447
|
if (!stepStarted.ok)
|
|
422
448
|
return fail("provider-error", LEAD_FAILURE_COPY.notStarted, false);
|
|
423
449
|
steps++;
|
|
424
|
-
progress({ kind: "step", turn: turnId, step: steps });
|
|
450
|
+
progress({ kind: "step", turn: turnId, step: steps, ...(nextReason === undefined ? {} : { reason: nextReason }) });
|
|
451
|
+
nextReason = undefined;
|
|
425
452
|
const requestBytes = Buffer.byteLength(outbound, "utf8");
|
|
426
453
|
let result;
|
|
427
454
|
// The turn owns its deadline (lead-stall, release check 2438): at the deadline the request is aborted, which ends a
|
|
@@ -430,7 +457,9 @@ export async function runLeadTurn(input) {
|
|
|
430
457
|
const controller = new AbortController();
|
|
431
458
|
const abortAt = setTimeout(() => controller.abort(), remainingMs);
|
|
432
459
|
const settle = (work) => settleBy(work, remainingMs + LEAD_ABORT_GRACE_MS)
|
|
433
|
-
|
|
460
|
+
// A harness that ended at the deadline still says whether it had written anything (recorded, never shown).
|
|
461
|
+
.then(value => value === LATE ? { ok: false, problem: "timeout" } : controller.signal.aborted
|
|
462
|
+
? { ok: false, problem: "timeout", ...(!value.ok && "quiet" in value && typeof value.quiet === "boolean" ? { quiet: value.quiet } : {}) } : value)
|
|
434
463
|
.finally(() => clearTimeout(abortAt));
|
|
435
464
|
if (direct) {
|
|
436
465
|
result = await settle(performLeadRequest(request, directProvider, controller.signal, fetcher)
|
|
@@ -440,7 +469,7 @@ export async function runLeadTurn(input) {
|
|
|
440
469
|
const runner = input.subscriptionRunner ?? performSubscriptionLeadRequest;
|
|
441
470
|
result = await settle((async () => runner({
|
|
442
471
|
provider: subscriptionProvider,
|
|
443
|
-
model:
|
|
472
|
+
model: stepModel,
|
|
444
473
|
system: LEAD_CONTRACT,
|
|
445
474
|
dataDocument: document,
|
|
446
475
|
history,
|
|
@@ -449,15 +478,22 @@ export async function runLeadTurn(input) {
|
|
|
449
478
|
signal: controller.signal,
|
|
450
479
|
// The reply as it is written; text that looks like a secret is
|
|
451
480
|
// never shown (the finished reply is scanned again before saving).
|
|
452
|
-
...(input.onProgress === undefined ? {} : { onText: (text) => { if (
|
|
453
|
-
|
|
481
|
+
...(input.onProgress === undefined ? {} : { onText: (text) => { if (controller.signal.aborted)
|
|
482
|
+
return; if (scanForSecrets(text).length === 0)
|
|
483
|
+
progress({ kind: "text", turn: turnId, step: steps, text });
|
|
484
|
+
else
|
|
485
|
+
blockPreview(); } }),
|
|
454
486
|
}))().catch(() => ({ ok: false, problem: "provider-error" })));
|
|
455
487
|
}
|
|
456
488
|
now = clock();
|
|
457
489
|
const finishStep = (outcome) => store.finalizeChatTurn(step.id, stepStarted.generation, outcome, now);
|
|
458
490
|
if (!result.ok) {
|
|
459
491
|
if (!direct) {
|
|
460
|
-
|
|
492
|
+
// The exact cause, saved (v158): what a later reader shows, and what the next timeout diagnosis reads.
|
|
493
|
+
const quiet = "quiet" in result ? result.quiet : undefined;
|
|
494
|
+
const detail = result.problem === "timeout" ? (quiet === true ? "timeout-silent" : quiet === false ? "timeout-output" : null)
|
|
495
|
+
: result.problem === "not-found" ? "not-installed" : result.problem === "malformed-reply" ? null : "sign-in";
|
|
496
|
+
finishStep({ state: "failed", failureReason: result.problem === "timeout" ? "timeout" : result.problem === "malformed-reply" ? "malformed-reply" : "provider-error", settledMicrousd: 0, failureDetail: detail });
|
|
461
497
|
const message = result.problem === "not-found"
|
|
462
498
|
? LEAD_FAILURE_COPY.notInstalled
|
|
463
499
|
: result.problem === "timeout"
|
|
@@ -466,29 +502,32 @@ export async function runLeadTurn(input) {
|
|
|
466
502
|
? LEAD_FAILURE_COPY.unreadable
|
|
467
503
|
: LEAD_FAILURE_COPY.signIn;
|
|
468
504
|
if (result.problem === "timeout")
|
|
469
|
-
return timedOut(false);
|
|
470
|
-
return fail(result.problem === "malformed-reply" ? "malformed-reply" : "provider-error", message, false);
|
|
505
|
+
return timedOut(false, detail);
|
|
506
|
+
return fail(result.problem === "malformed-reply" ? "malformed-reply" : "provider-error", message, false, detail);
|
|
471
507
|
}
|
|
472
508
|
if (result.problem.startsWith("status-")) {
|
|
473
509
|
// 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);
|
|
510
|
+
finishStep({ state: "failed", failureReason: "provider-error", settledMicrousd: 0, failureDetail: "refused" });
|
|
511
|
+
return fail("provider-error", LEAD_FAILURE_COPY.refused, false, "refused");
|
|
476
512
|
}
|
|
477
513
|
if (result.problem === "timeout") {
|
|
478
514
|
finishStep({ state: "failed", failureReason: "timeout", settledMicrousd: null, unknownSpend: true });
|
|
479
515
|
return timedOut(true);
|
|
480
516
|
}
|
|
481
517
|
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);
|
|
518
|
+
finishStep({ state: "failed", failureReason: "provider-error", settledMicrousd: null, unknownSpend: true, failureDetail: "lost" });
|
|
519
|
+
return fail("provider-error", LEAD_FAILURE_COPY.lostUnknownCost, true, "lost");
|
|
484
520
|
}
|
|
485
521
|
finishStep({ state: "failed", failureReason: "malformed-reply", settledMicrousd: null, unknownSpend: true });
|
|
486
522
|
return fail("malformed-reply", LEAD_FAILURE_COPY.unreadableUnknownCost, true);
|
|
487
523
|
}
|
|
488
524
|
const answer = result.answer;
|
|
489
525
|
// 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
|
-
|
|
526
|
+
if (!direct && input.onProgress !== undefined && !controller.signal.aborted && answer.text !== "") {
|
|
527
|
+
if (scanForSecrets(answer.text).length === 0)
|
|
528
|
+
progress({ kind: "text", turn: turnId, step: steps, text: answer.text, final: true });
|
|
529
|
+
else
|
|
530
|
+
blockPreview();
|
|
492
531
|
}
|
|
493
532
|
// Usage that cannot be true — more input tokens than bytes sent — is a
|
|
494
533
|
// malformed reply with unknown cost, never a number to settle by.
|
|
@@ -512,7 +551,9 @@ export async function runLeadTurn(input) {
|
|
|
512
551
|
cacheReadTokens: answer.cacheReadTokens ?? null, cacheWriteTokens: answer.cacheWriteTokens ?? null,
|
|
513
552
|
// v140: what the provider itself reported for this request (absent: unreported, kept NULL).
|
|
514
553
|
modelReported: answer.modelReported ?? null, providerTurns: answer.providerTurns ?? null, reportedCostUsd: answer.reportedCostUsd ?? null,
|
|
515
|
-
billingReported: answer.billingReported ?? null
|
|
554
|
+
billingReported: answer.billingReported ?? null,
|
|
555
|
+
// v158: the tools this step asked for, names only (responsive-chat.md §7.2), so the next latency call is data-driven.
|
|
556
|
+
tools: answer.calls.map(one => one.name) });
|
|
516
557
|
tokensIn += answer.tokensIn;
|
|
517
558
|
tokensOut += answer.tokensOut;
|
|
518
559
|
if (answer.cacheReadTokens === undefined)
|
|
@@ -527,6 +568,14 @@ export async function runLeadTurn(input) {
|
|
|
527
568
|
const changed = guard(await revalidated());
|
|
528
569
|
if (changed !== null)
|
|
529
570
|
return changed;
|
|
571
|
+
// A quick step that asked for more than a read (§8): its calls are discarded unrun, and the configured model takes the
|
|
572
|
+
// reply over from the same history, for this step and every later one. Steps and the turn's deadline still bind.
|
|
573
|
+
if (route === "fast" && quickStepEscalates(answer.calls)) {
|
|
574
|
+
route = "escalated";
|
|
575
|
+
setLeadTurnRoute(store, turnId, started.generation, route);
|
|
576
|
+
lastText = "";
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
530
579
|
if (answer.calls.length === 0) {
|
|
531
580
|
if (answer.text.trim() === "")
|
|
532
581
|
return fail("malformed-reply", LEAD_FAILURE_COPY.empty, false);
|
|
@@ -534,6 +583,7 @@ export async function runLeadTurn(input) {
|
|
|
534
583
|
const claim = unbacked(answer.text);
|
|
535
584
|
if (claim !== null && !repaired && steps < maxSteps) {
|
|
536
585
|
repaired = true;
|
|
586
|
+
nextReason = "repair";
|
|
537
587
|
history.push({ role: "assistant", text: answer.text, calls: [] });
|
|
538
588
|
history.push({ role: "operator", text: deliverableRepair(claim) });
|
|
539
589
|
continue;
|
|
@@ -541,6 +591,7 @@ export async function runLeadTurn(input) {
|
|
|
541
591
|
const candidate = claim === null ? answer.text : dropDeliverableClaims(answer.text);
|
|
542
592
|
if (replyLimit !== null && candidate.length > replyLimit && !shortened && steps < maxSteps) {
|
|
543
593
|
shortened = true;
|
|
594
|
+
nextReason = "shorten";
|
|
544
595
|
history.push({ role: "assistant", text: answer.text, calls: [] });
|
|
545
596
|
history.push({ role: "operator", text: shortenAsk([{ field: "Your reply", limit: replyLimit, length: candidate.length }]) });
|
|
546
597
|
continue;
|
|
@@ -566,9 +617,24 @@ export async function runLeadTurn(input) {
|
|
|
566
617
|
progress({ kind: "tool", turn: turnId, step: steps, id: progressId, label: leadToolLabel(call.name) });
|
|
567
618
|
// A registered tool this request did not advertise is refused before it is read or run (the only production
|
|
568
619
|
// caller of executeLeadTool): the model learns the road (find_tools, then the next step) and the reply goes on.
|
|
620
|
+
const toolContext = () => ({ store, who, now: clock(), draft, selectEvidence, ...(input.replyImage === undefined ? {} : { replyImage: input.replyImage }), step: steps, readDecisions, readResults, searchedMemory, checkedCapabilities, ask, findTools, ...(input.integrations === undefined ? {} : { integrations: input.integrations }), thread: thread.id, turn: turnId, ...(promiseChannelOfTurn(input.channel) === undefined ? {} : { channel: promiseChannelOfTurn(input.channel) }), ...(input.evidenceRoot === undefined ? {} : { evidenceRoot: input.evidenceRoot }), ...(input.mediaDelivery === undefined ? {} : { mediaDelivery: input.mediaDelivery }) });
|
|
621
|
+
// propose_start (docs/plans/one-step-chat.md C3), after the advertisement gate: its input is validated first (so
|
|
622
|
+
// invalid input never reaches the classifier), then Toolroll prepares this call's own preview in this turn (up to
|
|
623
|
+
// the classifier's budget), then the person's standing and the thread are re-proved after that wait, and only then
|
|
624
|
+
// does the tool run, with the preview in this call's own context. The model's arguments never name it.
|
|
625
|
+
let startPrepared;
|
|
626
|
+
if (call.name === "propose_start" && advertisedNames.has(call.name)) {
|
|
627
|
+
const checked = startInputOf(toolContext(), call.args);
|
|
628
|
+
if (checked.ok) {
|
|
629
|
+
startPrepared = await prepareChatStart(store, { actor: who.name, repo: checked.input.repo, title: checked.input.title, turn: turnId }, clock());
|
|
630
|
+
const changed = guard(await revalidated());
|
|
631
|
+
if (changed !== null)
|
|
632
|
+
return changed;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
569
635
|
const outcome = !advertisedNames.has(call.name)
|
|
570
636
|
? { ok: false, message: `${call.name} isn't available in this step. Call find_tools with what you want to do; tools it finds can be called from your next step.` }
|
|
571
|
-
: executeLeadTool({
|
|
637
|
+
: executeLeadTool({ ...toolContext(), ...(startPrepared === undefined ? {} : { startPrepared }) }, call.name, call.args, view);
|
|
572
638
|
let displayOutcome = { state: "succeeded" };
|
|
573
639
|
if (!outcome.ok) {
|
|
574
640
|
const reason = leadToolFailureReason(call.name, outcome.message);
|
|
@@ -598,10 +664,13 @@ export async function runLeadTurn(input) {
|
|
|
598
664
|
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
665
|
}
|
|
600
666
|
// Ruling 11: model text is scanned before it becomes durable.
|
|
601
|
-
if (scanForSecrets(reply).length > 0)
|
|
667
|
+
if (scanForSecrets(reply).length > 0) {
|
|
668
|
+
blockPreview();
|
|
602
669
|
return fail("secret-refused", LEAD_FAILURE_COPY.secretReply, false);
|
|
670
|
+
}
|
|
603
671
|
now = clock();
|
|
604
|
-
const
|
|
672
|
+
const routed = routeWords(route, quickModel, config.model);
|
|
673
|
+
const activity = `${activitySummary(reads, proposals, steps, cacheKnown && tokensIn > 0 ? Math.min(1, cacheRead / tokensIn) : null)}${routed === null ? "" : ` · ${routed}`}`;
|
|
605
674
|
// One write (finding 12): settle, debit, promote the drafts, append the assistant text.
|
|
606
675
|
const finalized = store.finalizeLeadTurn(turnId, started.generation, { state: "answered", settledMicrousd: settled, tokensIn, tokensOut, message: { text: reply, activity } }, now);
|
|
607
676
|
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>;
|