toolroll 0.9.55 → 0.9.56
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 +10 -0
- package/README.md +24 -8
- package/dist/action-ledger.d.ts +2 -4
- package/dist/action-ledger.js +2 -10
- package/dist/approval-policy.d.ts +5 -5
- package/dist/approval-policy.js +4 -4
- package/dist/approval-rules-ui.js +2 -2
- package/dist/browser/workspace.js +5 -5
- package/dist/browser-workspace.d.ts +12 -12
- package/dist/budget-alerts.js +3 -3
- package/dist/chat-actions.d.ts +23 -18
- package/dist/chat-actions.js +110 -88
- package/dist/chat-ask.d.ts +2 -2
- package/dist/chat-ask.js +5 -5
- package/dist/chat-channel.d.ts +15 -15
- package/dist/chat-channel.js +20 -20
- package/dist/chat-controls.d.ts +0 -4
- package/dist/chat-controls.js +0 -1
- package/dist/chat-core.js +1 -1
- package/dist/chat-delivery-state.js +2 -2
- package/dist/chat-delivery.d.ts +2 -2
- package/dist/chat-delivery.js +27 -34
- package/dist/chat-flow.js +3 -3
- package/dist/chat-messages.d.ts +1 -1
- package/dist/chat-messages.js +4 -4
- package/dist/chat-migration.d.ts +1 -1
- package/dist/chat-migration.js +11 -11
- package/dist/chat-rooms.js +3 -3
- package/dist/chat-warmth.d.ts +2 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +6 -7
- package/dist/contracts/chat-actions.d.ts +32 -27
- package/dist/contracts/chat-actions.js +29 -11
- package/dist/contracts/chat-content.d.ts +1 -1
- package/dist/contracts/chat-content.js +1 -1
- package/dist/contracts/chat-tables.js +4 -4
- package/dist/contracts/console-api.d.ts +6 -6
- package/dist/contracts/console-api.js +7 -7
- package/dist/contracts/flow-propose.d.ts +3 -3
- package/dist/contracts/flow.d.ts +132 -125
- package/dist/contracts/flow.js +27 -12
- package/dist/contracts/lead-context.d.ts +1 -1
- package/dist/contracts/lead-context.js +2 -2
- package/dist/contracts/lead-tools.d.ts +20 -18
- package/dist/contracts/lead-tools.js +15 -15
- package/dist/contracts/stage-output.d.ts +1 -1
- package/dist/contracts/stage-output.js +1 -1
- package/dist/contracts/store-json.d.ts +23 -22
- package/dist/contracts/store-json.js +17 -17
- package/dist/contracts/{teammate-turn.d.ts → subagent-turn.d.ts} +5 -5
- package/dist/contracts/{teammate-turn.js → subagent-turn.js} +7 -7
- package/dist/converse.d.ts +23 -23
- package/dist/converse.js +28 -28
- package/dist/coordinator-proposals.d.ts +2 -2
- package/dist/coordinator-proposals.js +1 -1
- package/dist/coordinator.js +1 -1
- package/dist/deprecations.d.ts +12 -0
- package/dist/deprecations.js +20 -0
- package/dist/desktop-update-gate.js +8 -4
- package/dist/desktop-update.js +6 -1
- package/dist/discord-chat.js +6 -6
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.js +1 -1
- package/dist/envelope.js +0 -1
- package/dist/export-ui.js +1 -1
- package/dist/export.js +7 -7
- package/dist/flow-engine.d.ts +2 -2
- package/dist/flow-engine.js +21 -21
- package/dist/flow-insights.d.ts +1 -1
- package/dist/flow-share.js +13 -8
- package/dist/flow-steps.d.ts +5 -5
- package/dist/flow-steps.js +19 -19
- package/dist/flow-triggers.d.ts +1 -1
- package/dist/flow-triggers.js +1 -1
- package/dist/flows-cli.js +1 -1
- package/dist/flows-ui.js +20 -20
- package/dist/flows.js +19 -19
- package/dist/guides.js +4 -3
- package/dist/invoke.js +1 -1
- package/dist/kits-ui.js +5 -5
- package/dist/kits.d.ts +12 -12
- package/dist/kits.js +31 -31
- package/dist/{mate-cli.d.ts → lead-cli.d.ts} +11 -11
- package/dist/{mate-cli.js → lead-cli.js} +38 -38
- package/dist/lead-commitments.js +4 -4
- package/dist/lead-context.d.ts +2 -2
- package/dist/lead-context.js +9 -9
- package/dist/{mate-contract.d.ts → lead-contract.d.ts} +3 -3
- package/dist/{mate-contract.js → lead-contract.js} +7 -6
- package/dist/{mate-doors.d.ts → lead-doors.d.ts} +6 -6
- package/dist/{mate-doors.js → lead-doors.js} +24 -24
- package/dist/lead-follow.d.ts +5 -5
- package/dist/lead-follow.js +9 -9
- package/dist/lead-people.d.ts +5 -5
- package/dist/lead-people.js +14 -14
- package/dist/{mate-progress.d.ts → lead-progress.d.ts} +9 -9
- package/dist/{mate-progress.js → lead-progress.js} +3 -3
- package/dist/{mate-tools.d.ts → lead-tools.d.ts} +19 -19
- package/dist/{mate-tools.js → lead-tools.js} +109 -101
- package/dist/{mate.d.ts → lead.d.ts} +27 -27
- package/dist/{mate.js → lead.js} +102 -102
- package/dist/limits-ui.d.ts +1 -1
- package/dist/limits-ui.js +1 -1
- package/dist/mcp-connect.d.ts +1 -1
- package/dist/mcp-connect.js +3 -3
- package/dist/mcp-core.js +1 -1
- package/dist/memory-pass.d.ts +2 -2
- package/dist/memory-pass.js +7 -7
- package/dist/notification-rows.d.ts +2 -2
- package/dist/operate.d.ts +3 -3
- package/dist/operate.js +35 -34
- package/dist/policy.d.ts +1 -1
- package/dist/principal.d.ts +1 -1
- package/dist/principal.js +2 -2
- package/dist/project-delete-ui.js +1 -1
- package/dist/project-delete.d.ts +1 -1
- package/dist/project-delete.js +26 -26
- package/dist/project-memory.js +1 -1
- package/dist/project-tools.d.ts +1 -1
- package/dist/project-tools.js +1 -1
- package/dist/recipe-ui.js +1 -1
- package/dist/retention.js +4 -4
- package/dist/scope.d.ts +2 -2
- package/dist/scout-report.d.ts +1 -1
- package/dist/scout-report.js +1 -1
- package/dist/serve.js +40 -45
- package/dist/server/chat.d.ts +4 -4
- package/dist/server/chat.js +97 -97
- package/dist/server/chrome.js +3 -3
- package/dist/server/flows.js +69 -62
- package/dist/server/guards.js +3 -3
- package/dist/server/http.d.ts +3 -3
- package/dist/server/http.js +1 -1
- package/dist/server/live.js +6 -6
- package/dist/server/people-tokens.js +1 -1
- package/dist/server/remote.js +12 -10
- package/dist/server/render-chat.d.ts +30 -30
- package/dist/server/render-chat.js +33 -33
- package/dist/server/render-pages.js +2 -2
- package/dist/server/render-settings.d.ts +7 -0
- package/dist/server/render-settings.js +3 -0
- package/dist/server/route-table.js +9 -7
- package/dist/server/runtime.d.ts +13 -13
- package/dist/server/settings.js +14 -13
- package/dist/server/tasks.js +6 -5
- package/dist/setup-guide.d.ts +2 -2
- package/dist/slack-chat.d.ts +2 -2
- package/dist/slack-chat.js +6 -6
- package/dist/spend-ui.d.ts +3 -3
- package/dist/spend-ui.js +9 -9
- package/dist/spend.d.ts +14 -14
- package/dist/spend.js +31 -31
- package/dist/store-baseline.d.ts +19 -0
- package/dist/store-baseline.js +2873 -0
- package/dist/store.d.ts +197 -249
- package/dist/store.js +747 -3084
- package/dist/subagent-admin.d.ts +53 -0
- package/dist/{teammate-admin.js → subagent-admin.js} +37 -37
- package/dist/{teammate-desk.d.ts → subagent-desk.d.ts} +22 -23
- package/dist/{teammate-desk.js → subagent-desk.js} +26 -35
- package/dist/{teammate-memory.d.ts → subagent-memory.d.ts} +17 -17
- package/dist/{teammate-memory.js → subagent-memory.js} +41 -41
- package/dist/{teammate-question.d.ts → subagent-question.d.ts} +6 -6
- package/dist/{teammate-question.js → subagent-question.js} +17 -17
- package/dist/{teammate-tools.d.ts → subagent-tools.d.ts} +18 -18
- package/dist/{teammate-tools.js → subagent-tools.js} +36 -36
- package/dist/{teammate-week.d.ts → subagent-week.d.ts} +10 -10
- package/dist/{teammate-week.js → subagent-week.js} +23 -23
- package/dist/{teammate-work.d.ts → subagent-work.d.ts} +21 -21
- package/dist/{teammate-work.js → subagent-work.js} +50 -50
- package/dist/subagents-ui.d.ts +22 -0
- package/dist/{teammates-ui.js → subagents-ui.js} +45 -45
- package/dist/{teammates.d.ts → subagents.d.ts} +11 -11
- package/dist/{teammates.js → subagents.js} +21 -21
- package/dist/subscription-chat.d.ts +13 -13
- package/dist/subscription-chat.js +19 -19
- package/dist/surface.d.ts +19 -2
- package/dist/surface.js +7 -31
- package/dist/team-cli.d.ts +1 -0
- package/dist/team-cli.js +14 -2
- package/dist/team-leads.js +13 -13
- package/dist/team-runtime.d.ts +2 -2
- package/dist/team-runtime.js +8 -8
- package/dist/teams-chat.js +6 -6
- package/dist/telegram-flow.d.ts +3 -3
- package/dist/telegram-flow.js +3 -3
- package/dist/{telegram-mate.d.ts → telegram-lead.d.ts} +5 -5
- package/dist/{telegram-mate.js → telegram-lead.js} +27 -27
- package/dist/telegram-status.js +1 -1
- package/dist/telegram-team.d.ts +1 -1
- package/dist/telegram-team.js +5 -5
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +9 -30
- package/dist/text-limits.d.ts +18 -18
- package/dist/text-limits.js +18 -18
- package/dist/toolroll-update.d.ts +8 -0
- package/dist/toolroll-update.js +45 -8
- package/package.json +1 -2
- package/dist/teammate-admin.d.ts +0 -53
- package/dist/teammates-ui.d.ts +0 -21
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import type { ChatConfig,
|
|
1
|
+
import { type LeadProgress } from "./lead-progress.js";
|
|
2
|
+
import type { ChatConfig, LeadSession, LeadThread, Store } from "./store.js";
|
|
3
3
|
import type { Integration } from "./integrations.js";
|
|
4
4
|
import type { VerifiedApprover } from "./principal.js";
|
|
5
|
-
import { type
|
|
6
|
-
import { type
|
|
5
|
+
import { type LeadHistoryMessage } from "./converse.js";
|
|
6
|
+
import { type SubscriptionLeadRunner } from "./subscription-chat.js";
|
|
7
7
|
import { type LeadChannel } from './lead-context.js';
|
|
8
|
-
export declare const
|
|
8
|
+
export declare const LEAD_MESSAGE_MAX_CHARS = 2000;
|
|
9
9
|
/** The thread's recent history the model sees, most recent first until the cap. */
|
|
10
|
-
export declare const
|
|
11
|
-
export declare const
|
|
12
|
-
export type
|
|
10
|
+
export declare const LEAD_HISTORY_CAP_BYTES = 16384;
|
|
11
|
+
export declare const LEAD_HISTORY_MAX_MESSAGES = 40;
|
|
12
|
+
export type LeadTurnInput = {
|
|
13
13
|
store: Store;
|
|
14
14
|
who: VerifiedApprover;
|
|
15
|
-
session:
|
|
16
|
-
thread:
|
|
15
|
+
session: LeadSession;
|
|
16
|
+
thread: LeadThread;
|
|
17
17
|
/** The installation's chat configuration: provider, model, pinned price, caps. */
|
|
18
18
|
config: ChatConfig;
|
|
19
19
|
/** Direct API key; subscription providers use their cached harness login. */
|
|
@@ -33,11 +33,11 @@ export type MateTurnInput = {
|
|
|
33
33
|
context?: string;
|
|
34
34
|
fetcher?: typeof fetch;
|
|
35
35
|
/** Injected by tests; production invokes the isolated local harness. */
|
|
36
|
-
subscriptionRunner?:
|
|
36
|
+
subscriptionRunner?: SubscriptionLeadRunner;
|
|
37
37
|
/** Live progress for someone watching (chat streaming): the steps, the
|
|
38
38
|
* tools in plain words, and the reply as it is written. Display only;
|
|
39
39
|
* never called with text that looks like a secret. */
|
|
40
|
-
onProgress?: (event:
|
|
40
|
+
onProgress?: (event: LeadProgress) => void;
|
|
41
41
|
clock?: () => Date;
|
|
42
42
|
/** Where evidence lives — get_task reads a scout's report from here. */
|
|
43
43
|
evidenceRoot?: string;
|
|
@@ -62,14 +62,14 @@ export type MateTurnInput = {
|
|
|
62
62
|
ok: true;
|
|
63
63
|
} | {
|
|
64
64
|
ok: false;
|
|
65
|
-
reason:
|
|
65
|
+
reason: LeadChannelProblem;
|
|
66
66
|
}>;
|
|
67
67
|
};
|
|
68
|
-
/** Why a chat's standing changed under a turn: each has its own plain words (
|
|
69
|
-
export type
|
|
70
|
-
export type
|
|
71
|
-
export type
|
|
72
|
-
export type
|
|
68
|
+
/** Why a chat's standing changed under a turn: each has its own plain words (LEAD_CHANNEL_COPY). */
|
|
69
|
+
export type LeadChannelProblem = "unpaired" | "not-approver" | "projects-changed" | "projects-unreadable" | "member-changed" | "access-changed";
|
|
70
|
+
export type LeadRefusal = "empty-message" | "secret-in-message" | "secret-in-context" | "standing" | "unpriced" | "ceiling-changed" | "not-yours" | "thread-closed" | "latched" | "concurrent" | "daily-cap" | "session-exhausted" | "session-ended" | "over-budget" | "monthly-budget" | "policy" | "invalid-request" | "request-changed" | "channel";
|
|
71
|
+
export type LeadFailure = "provider-error" | "timeout" | "malformed-reply" | "secret-refused" | "latched" | "revoked" | "superseded";
|
|
72
|
+
export type LeadTurnOutcome = {
|
|
73
73
|
ok: true;
|
|
74
74
|
replayed?: false;
|
|
75
75
|
turn: number;
|
|
@@ -85,22 +85,22 @@ export type MateTurnOutcome = {
|
|
|
85
85
|
turn: number;
|
|
86
86
|
} | {
|
|
87
87
|
ok: false;
|
|
88
|
-
refused:
|
|
88
|
+
refused: LeadRefusal;
|
|
89
89
|
message: string;
|
|
90
90
|
} | {
|
|
91
91
|
ok: false;
|
|
92
92
|
turn: number;
|
|
93
|
-
failed:
|
|
93
|
+
failed: LeadFailure;
|
|
94
94
|
message: string;
|
|
95
95
|
unknownSpend: boolean;
|
|
96
96
|
saved?: boolean;
|
|
97
97
|
};
|
|
98
98
|
/** What happened, what it means, and one next step: plain words a person can act on, never internal terms. */
|
|
99
|
-
export declare const
|
|
99
|
+
export declare const LEAD_REFUSAL_COPY: Record<LeadRefusal, string>;
|
|
100
100
|
/** A turn stopped because its chat's standing changed: what happened, what it means, one next step. */
|
|
101
|
-
export declare const
|
|
101
|
+
export declare const LEAD_CHANNEL_COPY: Record<LeadChannelProblem, string>;
|
|
102
102
|
/** Why a reply did not finish, in the same three parts: what happened, what it means, one next step. */
|
|
103
|
-
export declare const
|
|
103
|
+
export declare const LEAD_FAILURE_COPY: {
|
|
104
104
|
readonly stopped: "This reply was stopped before it finished, from another window or by a newer message. Nothing it proposed was kept. Send your message again if you still need an answer.";
|
|
105
105
|
readonly ended: "Your sign-in or this conversation ended while the lead was answering. Nothing it proposed was kept. Sign in again or start a new chat, then send your message again.";
|
|
106
106
|
readonly tooLong: string;
|
|
@@ -119,12 +119,12 @@ export declare const MATE_FAILURE_COPY: {
|
|
|
119
119
|
readonly unusable: "The lead's answer couldn't be used, so it was thrown away. Nothing was kept. Send your message again.";
|
|
120
120
|
};
|
|
121
121
|
/** A failed turn's saved reason as plain words, for a surface that reads the turn back later (a restart, a delivery retry). */
|
|
122
|
-
export declare function
|
|
122
|
+
export declare function leadFailureText(reason: string | null): string;
|
|
123
123
|
/** How long a provider that ignored its abort at the turn's deadline is waited for before the turn ends without it.
|
|
124
124
|
* Its process group was already killed at the deadline; this only bounds the wait for the transport to say so. */
|
|
125
|
-
export declare const
|
|
125
|
+
export declare const LEAD_ABORT_GRACE_MS = 5000;
|
|
126
126
|
/** The last messages of the thread as provider-neutral history, newest kept first until the byte cap. */
|
|
127
|
-
export declare function historyFor(store: Store, thread: number, queuedMessageId?: number):
|
|
127
|
+
export declare function historyFor(store: Store, thread: number, queuedMessageId?: number): LeadHistoryMessage[];
|
|
128
128
|
/** "read 3 · proposed 1 · 2 steps" — counts only, never results (ruling 11). */
|
|
129
129
|
export declare function activitySummary(reads: number, proposals: number, steps: number): string;
|
|
130
|
-
export declare function
|
|
130
|
+
export declare function runLeadTurn(input: LeadTurnInput): Promise<LeadTurnOutcome>;
|
package/dist/{mate.js → lead.js}
RENAMED
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
-
import {
|
|
2
|
+
import { leadToolFailureReason, leadToolLabel } from "./lead-progress.js";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import {
|
|
4
|
+
import { leadTimeoutNotice } from "./store.js";
|
|
5
5
|
import { RESULT_IMAGES_PER_TURN_CAP } from "./chat-evidence.js";
|
|
6
6
|
import { isVerifiedApprover, reproveApprover } from "./principal.js";
|
|
7
|
-
import {
|
|
7
|
+
import { LEAD_MAX_CALLS_PER_STEP, LEAD_MAX_STEPS, LEAD_TOOL_RESULT_CAP_BYTES, TURN_WALL_CLOCK_MS, composeLeadRequest, credentialKeyOf, isDirectChatProvider, leadWorstCaseForPrice, performLeadRequest, priceForConfig, settleForPrice, subscriptionCredentialKey, } from "./converse.js";
|
|
8
8
|
import { redactSecretLines, scanForSecrets } from "./evidence.js";
|
|
9
9
|
import { redactSecretAssignments } from "./builder.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
10
|
+
import { LEAD_CONTRACT } from "./lead-contract.js";
|
|
11
|
+
import { LEAD_MAX_PROPOSALS_PER_TURN, LEAD_TOOL_SCHEMAS, executeLeadTool, isLeadTool, leadViewContextFor, projectLabelForLead, redactForLead, toolResultBytes } from "./lead-tools.js";
|
|
12
|
+
import { composeSubscriptionLeadPrompt, performSubscriptionLeadRequest } from "./subscription-chat.js";
|
|
13
13
|
import { LEAD_REPLY_LIMITS, leadContext } from './lead-context.js';
|
|
14
14
|
import { shortenAsk } from './text-limits.js';
|
|
15
15
|
import { envValue } from "./names.js";
|
|
16
16
|
import { deliverableClaim, deliverableRepair, dropDeliverableClaims, replyCarriesDeliverable } from "./reply-shape.js";
|
|
17
|
-
export const
|
|
17
|
+
export const LEAD_MESSAGE_MAX_CHARS = 2_000;
|
|
18
18
|
/** Where a promise made this turn is reported: the chat app it arrived on, or this conversation for the console and terminal. */
|
|
19
19
|
function promiseChannelOfTurn(channel) {
|
|
20
20
|
return channel === undefined ? undefined : channel === "console" || channel === "terminal" ? "chat" : channel;
|
|
21
21
|
}
|
|
22
22
|
/** The thread's recent history the model sees, most recent first until the cap. */
|
|
23
|
-
export const
|
|
24
|
-
export const
|
|
23
|
+
export const LEAD_HISTORY_CAP_BYTES = 16_384;
|
|
24
|
+
export const LEAD_HISTORY_MAX_MESSAGES = 40;
|
|
25
25
|
/** What happened, what it means, and one next step: plain words a person can act on, never internal terms. */
|
|
26
|
-
export const
|
|
27
|
-
"empty-message": `That message was empty or too long, so it wasn't sent. Write 1 to ${
|
|
26
|
+
export const LEAD_REFUSAL_COPY = {
|
|
27
|
+
"empty-message": `That message was empty or too long, so it wasn't sent. Write 1 to ${LEAD_MESSAGE_MAX_CHARS} characters and send it again.`,
|
|
28
28
|
"secret-in-message": "That message looks like it contains a password or key, so it wasn't sent or saved. Remove it and send the message again.",
|
|
29
29
|
"secret-in-context": "Something saved in your projects looks like a password or key, so the lead can't read your projects safely. Remove it from the task or note it's in, then try again.",
|
|
30
30
|
standing: "Your sign-in changed, so the lead can't act for you right now. Sign in again.",
|
|
@@ -45,7 +45,7 @@ export const MATE_REFUSAL_COPY = {
|
|
|
45
45
|
channel: "This conversation's connection changed, so the message wasn't sent. Reconnect it, then send again.",
|
|
46
46
|
};
|
|
47
47
|
/** A turn stopped because its chat's standing changed: what happened, what it means, one next step. */
|
|
48
|
-
export const
|
|
48
|
+
export const LEAD_CHANNEL_COPY = {
|
|
49
49
|
unpaired: "This chat is no longer connected to your account, so the reply was stopped. Nothing it proposed was kept. Connect the chat again in Settings, then send your message again.",
|
|
50
50
|
"not-approver": "Your account can no longer approve work here, so the reply was stopped. Nothing it proposed was kept. Ask an owner to restore your access, then send your message again.",
|
|
51
51
|
"projects-changed": "The projects this chat can see changed while the lead was answering, so the reply was stopped. Nothing it proposed was kept. Send your message again.",
|
|
@@ -54,10 +54,10 @@ export const MATE_CHANNEL_COPY = {
|
|
|
54
54
|
"access-changed": "This chat's access changed while the lead was answering, so the reply was stopped. Nothing it proposed was kept. Reconnect the chat, then send your message again.",
|
|
55
55
|
};
|
|
56
56
|
/** Why a reply did not finish, in the same three parts: what happened, what it means, one next step. */
|
|
57
|
-
export const
|
|
57
|
+
export const LEAD_FAILURE_COPY = {
|
|
58
58
|
stopped: "This reply was stopped before it finished, from another window or by a newer message. Nothing it proposed was kept. Send your message again if you still need an answer.",
|
|
59
59
|
ended: "Your sign-in or this conversation ended while the lead was answering. Nothing it proposed was kept. Sign in again or start a new chat, then send your message again.",
|
|
60
|
-
tooLong:
|
|
60
|
+
tooLong: leadTimeoutNotice(false),
|
|
61
61
|
secretRead: "Something the lead read looked like a password or key, so it stopped before sending it anywhere. Nothing was kept. Remove the password or key from that task or note, then ask again.",
|
|
62
62
|
secretReply: "The lead's reply contained something that looked like a password or key, so it was thrown away. Nothing was kept. Ask again, without asking for a password or key.",
|
|
63
63
|
paused: "An earlier reply stopped before its cost was known, so chat is paused. This message wasn't answered. Confirm that earlier reply on the Chat page, then send again.",
|
|
@@ -66,29 +66,29 @@ export const MATE_FAILURE_COPY = {
|
|
|
66
66
|
unreadable: "The chat provider sent back an answer that couldn't be read, so it was thrown away. Nothing was kept. Send your message again.",
|
|
67
67
|
signIn: "The chat provider couldn't answer; its sign-in may have expired. Nothing was kept. Sign in to it again on this computer, then send your message again.",
|
|
68
68
|
refused: "The chat provider turned the request down. Nothing was kept or charged. Try again in a minute; if it keeps happening, check the provider in Settings → Lead.",
|
|
69
|
-
tooLongUnknownCost:
|
|
69
|
+
tooLongUnknownCost: leadTimeoutNotice(false, true),
|
|
70
70
|
lostUnknownCost: "The chat provider stopped responding partway through, and the cost isn't known, so chat is paused. Nothing it proposed was kept. Confirm the cost on the Chat page to turn chat back on.",
|
|
71
71
|
unreadableUnknownCost: "The chat provider sent back an answer that couldn't be read, and its cost isn't known, so chat is paused. Nothing it proposed was kept. Confirm the cost on the Chat page to turn chat back on.",
|
|
72
72
|
empty: "The lead sent back an empty answer. Nothing was kept. Send your message again.",
|
|
73
73
|
unusable: "The lead's answer couldn't be used, so it was thrown away. Nothing was kept. Send your message again.",
|
|
74
74
|
};
|
|
75
75
|
/** A failed turn's saved reason as plain words, for a surface that reads the turn back later (a restart, a delivery retry). */
|
|
76
|
-
export function
|
|
76
|
+
export function leadFailureText(reason) {
|
|
77
77
|
switch (reason) {
|
|
78
78
|
case "timeout":
|
|
79
|
-
case "crashed": return
|
|
80
|
-
case "malformed-reply": return
|
|
81
|
-
case "secret-refused": return
|
|
82
|
-
case "latched": return
|
|
83
|
-
case "revoked": return
|
|
84
|
-
case "superseded": return
|
|
85
|
-
case "provider-error": return
|
|
79
|
+
case "crashed": return LEAD_FAILURE_COPY.tooLong;
|
|
80
|
+
case "malformed-reply": return LEAD_FAILURE_COPY.unreadable;
|
|
81
|
+
case "secret-refused": return LEAD_FAILURE_COPY.secretReply;
|
|
82
|
+
case "latched": return LEAD_FAILURE_COPY.paused;
|
|
83
|
+
case "revoked": return LEAD_FAILURE_COPY.ended;
|
|
84
|
+
case "superseded": return LEAD_FAILURE_COPY.stopped;
|
|
85
|
+
case "provider-error": return LEAD_FAILURE_COPY.refused;
|
|
86
86
|
default: return "The lead's reply didn't finish. Nothing it proposed was kept. Send your message again.";
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
/** How long a provider that ignored its abort at the turn's deadline is waited for before the turn ends without it.
|
|
90
90
|
* Its process group was already killed at the deadline; this only bounds the wait for the transport to say so. */
|
|
91
|
-
export const
|
|
91
|
+
export const LEAD_ABORT_GRACE_MS = 5_000;
|
|
92
92
|
const LATE = Symbol("late");
|
|
93
93
|
/** The work's own result, or LATE once `ms` passes: the turn's clock, not the work, decides when the person hears back. */
|
|
94
94
|
function settleBy(work, ms) {
|
|
@@ -99,7 +99,7 @@ function settleBy(work, ms) {
|
|
|
99
99
|
const READ_TOOLS = new Set(["get_brief", "get_project_context", "get_actions", "get_action_status", "get_skills", "get_acceptance_evidence", "recap", "list_repos", "list_tasks", "get_task", "get_result", "get_result_images", "get_controls", "get_agents", "get_project_knowledge", "get_task_conversation", "get_diff", "get_check_log", "get_project_tools", "get_flows", "get_flow_insights", "list_decisions", "get_decision", "queue"]);
|
|
100
100
|
/** The last messages of the thread as provider-neutral history, newest kept first until the byte cap. */
|
|
101
101
|
export function historyFor(store, thread, queuedMessageId) {
|
|
102
|
-
const rows = store.
|
|
102
|
+
const rows = store.listLeadMessages(thread, LEAD_HISTORY_MAX_MESSAGES);
|
|
103
103
|
const kept = [];
|
|
104
104
|
let bytes = 0;
|
|
105
105
|
for (let index = rows.length - 1; index >= 0; index--) {
|
|
@@ -109,9 +109,9 @@ export function historyFor(store, thread, queuedMessageId) {
|
|
|
109
109
|
if (queuedMessageId !== undefined && (row.id === queuedMessageId || row.role === 'operator' && row.turn === null))
|
|
110
110
|
continue;
|
|
111
111
|
bytes += Buffer.byteLength(row.text, "utf8");
|
|
112
|
-
if (bytes >
|
|
112
|
+
if (bytes > LEAD_HISTORY_CAP_BYTES)
|
|
113
113
|
break;
|
|
114
|
-
const author = queuedMessageId !== undefined && row.role === 'operator' && row.turn !== null ? store.
|
|
114
|
+
const author = queuedMessageId !== undefined && row.role === 'operator' && row.turn !== null ? store.getLeadTurn(row.turn)?.approver : undefined;
|
|
115
115
|
kept.unshift(row.role === "operator" ? { role: "operator", text: author ? `From ${author}:\n${row.text}` : row.text } : { role: "assistant", text: row.text, calls: [] });
|
|
116
116
|
}
|
|
117
117
|
// A history must open with the operator: a leading assistant reply without its question is dropped.
|
|
@@ -126,17 +126,17 @@ export function activitySummary(reads, proposals, steps) {
|
|
|
126
126
|
/** A tool result as the model sees it, measured as embedded; over the cap it becomes a typed refusal. */
|
|
127
127
|
function capped(value) {
|
|
128
128
|
const text = JSON.stringify(value);
|
|
129
|
-
if (toolResultBytes(text) <=
|
|
129
|
+
if (toolResultBytes(text) <= LEAD_TOOL_RESULT_CAP_BYTES)
|
|
130
130
|
return text;
|
|
131
131
|
return JSON.stringify({ ok: false, message: "that result is over the size cap — ask for less: one project, one state, or a smaller limit" });
|
|
132
132
|
}
|
|
133
|
-
export async function
|
|
133
|
+
export async function runLeadTurn(input) {
|
|
134
134
|
const { store, who, session, thread, config } = input;
|
|
135
135
|
const clock = input.clock ?? (() => new Date());
|
|
136
136
|
const fetcher = input.fetcher ?? fetch;
|
|
137
|
-
const refuse = (refused) => ({ ok: false, refused, message:
|
|
137
|
+
const refuse = (refused) => ({ ok: false, refused, message: LEAD_REFUSAL_COPY[refused] });
|
|
138
138
|
const message = input.message.trim();
|
|
139
|
-
if (message === "" || message.length >
|
|
139
|
+
if (message === "" || message.length > LEAD_MESSAGE_MAX_CHARS)
|
|
140
140
|
return refuse("empty-message");
|
|
141
141
|
// Secrets refuse BEFORE any row or request exists — nothing stored, nothing sent.
|
|
142
142
|
if (scanForSecrets(message).length > 0)
|
|
@@ -170,8 +170,8 @@ export async function runMateTurn(input) {
|
|
|
170
170
|
return refuse("invalid-request");
|
|
171
171
|
// Replays are read-only, but still require today's authority, not the
|
|
172
172
|
// session/thread snapshots supplied by a previous browser page.
|
|
173
|
-
const liveSession = store.
|
|
174
|
-
const liveThread = store.
|
|
173
|
+
const liveSession = store.getLeadSession(session.id);
|
|
174
|
+
const liveThread = store.getLeadThread(thread.id);
|
|
175
175
|
const sharedThread = liveThread !== null && store.canUseTeamMateThread(who.name, who.generation, liveThread.id);
|
|
176
176
|
if (liveSession?.approver !== who.name || (liveThread?.approver !== who.name && !sharedThread) || liveSession.credentialKey !== credentialKey)
|
|
177
177
|
return refuse("not-yours");
|
|
@@ -184,28 +184,28 @@ export async function runMateTurn(input) {
|
|
|
184
184
|
if (liveSession.ceilingDigest !== who.ceilingDigest || liveThread.ceilingDigest !== who.ceilingDigest)
|
|
185
185
|
return refuse("ceiling-changed");
|
|
186
186
|
const digest = createHash("sha256").update(JSON.stringify([thread.id, message, input.context ?? null])).digest("hex");
|
|
187
|
-
const receipt = request === undefined ? null : store.
|
|
187
|
+
const receipt = request === undefined ? null : store.leadRequestReceipt(session.id, request);
|
|
188
188
|
if (receipt !== null)
|
|
189
189
|
return receipt.digest === digest ? { ok: true, replayed: true, turn: receipt.turn } : refuse("request-changed");
|
|
190
190
|
// The channel's standing, proved before anything is admitted or sent.
|
|
191
191
|
if (input.revalidate !== undefined && !(await input.revalidate()).ok)
|
|
192
192
|
return refuse("channel");
|
|
193
193
|
let now = clock();
|
|
194
|
-
store.
|
|
195
|
-
const view =
|
|
194
|
+
store.sweepStaleLeadTurns(now);
|
|
195
|
+
const view = leadViewContextFor(store, who);
|
|
196
196
|
// Names in the bundle are deliberate (the lead's, the person's first name, project labels); everything else is scrubbed.
|
|
197
|
-
const document = leadContext(store, who.repos, now, { owner: who.name, thread: thread.id, redact: text =>
|
|
198
|
-
projectName: (path, index) =>
|
|
197
|
+
const document = leadContext(store, who.repos, now, { owner: who.name, thread: thread.id, redact: text => redactForLead(text, view),
|
|
198
|
+
projectName: (path, index) => projectLabelForLead(path, index, view.names),
|
|
199
199
|
...(input.evidenceRoot === undefined ? {} : { evidenceRoot: input.evidenceRoot }), ...(input.channel === undefined ? {} : { channel: input.channel }), ...(input.leadName === undefined ? {} : { leadName: input.leadName }) });
|
|
200
200
|
const authoredMessage = input.queuedMessageId === undefined ? message : `From ${who.name}:\n${message}`;
|
|
201
|
-
const historyMessage = input.context === undefined ? authoredMessage : `${
|
|
201
|
+
const historyMessage = input.context === undefined ? authoredMessage : `${redactForLead(input.context, view)}\n\n${authoredMessage}`;
|
|
202
202
|
const history = [...historyFor(store, thread.id, input.queuedMessageId), { role: "operator", text: historyMessage }];
|
|
203
203
|
const composeDirect = (key) => {
|
|
204
204
|
if (!direct)
|
|
205
205
|
throw new Error("not a direct chat provider");
|
|
206
|
-
return
|
|
206
|
+
return composeLeadRequest({ provider: directProvider, model: config.model, key, system: LEAD_CONTRACT, dataDocument: document, history, tools: LEAD_TOOL_SCHEMAS });
|
|
207
207
|
};
|
|
208
|
-
const composeSubscription = () =>
|
|
208
|
+
const composeSubscription = () => composeSubscriptionLeadPrompt({ system: LEAD_CONTRACT, dataDocument: document, history, tools: LEAD_TOOL_SCHEMAS });
|
|
209
209
|
// The exact outbound base is scanned whole. Only direct API traffic needs
|
|
210
210
|
// a worst-case dollar reservation; subscription traffic records zero.
|
|
211
211
|
const base = direct ? composeDirect("").body : composeSubscription();
|
|
@@ -214,9 +214,9 @@ export async function runMateTurn(input) {
|
|
|
214
214
|
// Fit the bounded tool loop to the already-authorized remaining allowance.
|
|
215
215
|
// Context/history growth must not demand a larger budget for a simple reply.
|
|
216
216
|
// Each chosen step retains the same worst-case call/output accounting.
|
|
217
|
-
let maxSteps =
|
|
217
|
+
let maxSteps = LEAD_MAX_STEPS;
|
|
218
218
|
const baseBytes = Buffer.byteLength(base, "utf8");
|
|
219
|
-
const reserveFor = (steps) => direct ?
|
|
219
|
+
const reserveFor = (steps) => direct ? leadWorstCaseForPrice(price, baseBytes, { steps }) : 0;
|
|
220
220
|
if (direct) {
|
|
221
221
|
const remaining = Math.min(liveSession.ceilingMicrousd - liveSession.spentMicrousd, config.weeklyCeilingMicrousd - store.chatWeeklySpendMicrousd(credentialKey, now));
|
|
222
222
|
while (maxSteps > 1 && reserveFor(maxSteps) > remaining)
|
|
@@ -224,12 +224,12 @@ export async function runMateTurn(input) {
|
|
|
224
224
|
}
|
|
225
225
|
const reserved = reserveFor(maxSteps);
|
|
226
226
|
const admitted = store.transact(() => {
|
|
227
|
-
const existing = request === undefined ? null : store.
|
|
227
|
+
const existing = request === undefined ? null : store.leadRequestReceipt(session.id, request);
|
|
228
228
|
if (existing !== null)
|
|
229
229
|
return existing.digest === digest
|
|
230
230
|
? { ok: true, replayed: true, turnId: existing.turn }
|
|
231
231
|
: { ok: false, reason: "request-changed" };
|
|
232
|
-
const opened = store.
|
|
232
|
+
const opened = store.openLeadTurn({
|
|
233
233
|
approver: who.name,
|
|
234
234
|
session: session.id,
|
|
235
235
|
thread: thread.id,
|
|
@@ -243,13 +243,13 @@ export async function runMateTurn(input) {
|
|
|
243
243
|
if (!opened.ok)
|
|
244
244
|
return opened;
|
|
245
245
|
const turnId = opened.id;
|
|
246
|
-
const started = store.
|
|
246
|
+
const started = store.startLeadTurn(turnId, now);
|
|
247
247
|
if (!started.ok)
|
|
248
|
-
throw new Error("new
|
|
248
|
+
throw new Error("new lead turn could not start");
|
|
249
249
|
if (input.queuedMessageId === undefined)
|
|
250
|
-
store.
|
|
250
|
+
store.appendLeadMessage({ thread: thread.id, turn: turnId, role: "operator", text: message }, now);
|
|
251
251
|
else {
|
|
252
|
-
const bound = store.handle.prepare("UPDATE
|
|
252
|
+
const bound = store.handle.prepare("UPDATE lead_message SET turn=? WHERE id=? AND thread=? AND role='operator' AND turn IS NULL AND text=?")
|
|
253
253
|
.run(turnId, input.queuedMessageId, thread.id, message);
|
|
254
254
|
if (Number(bound.changes) !== 1)
|
|
255
255
|
throw new Error('The queued message changed before admission.');
|
|
@@ -279,63 +279,63 @@ export async function runMateTurn(input) {
|
|
|
279
279
|
const searchedMemory = new Map();
|
|
280
280
|
const checkedCapabilities = new Map();
|
|
281
281
|
/** The turn's one question to its owner: shown with the reply only once the turn answers; a failed turn drops it. */
|
|
282
|
-
const ask = (question, options) => store.
|
|
282
|
+
const ask = (question, options) => store.recordLeadAsk({ turn: turnId, thread: thread.id, question, options }, clock());
|
|
283
283
|
let tokensIn = 0;
|
|
284
284
|
let tokensOut = 0;
|
|
285
285
|
let settled = 0;
|
|
286
286
|
const draft = (kind, payload) => {
|
|
287
|
-
if (proposals >=
|
|
287
|
+
if (proposals >= LEAD_MAX_PROPOSALS_PER_TURN)
|
|
288
288
|
return null;
|
|
289
289
|
proposals++;
|
|
290
|
-
return store.
|
|
290
|
+
return store.draftLeadProposal({ thread: thread.id, turn: turnId, kind, payload, ceilingDigest: who.ceilingDigest }, clock());
|
|
291
291
|
};
|
|
292
292
|
/** The screenshots a tool selected, kept under THIS turn: a failed turn deletes them with its drafts; a channel plans sends only from an answered turn. */
|
|
293
293
|
const selectEvidence = (rows) => {
|
|
294
|
-
store.
|
|
295
|
-
if (store.
|
|
294
|
+
store.recordLeadTurnEvidence(turnId, rows, RESULT_IMAGES_PER_TURN_CAP, clock());
|
|
295
|
+
if (store.getLeadTurn(turnId)?.state !== "running")
|
|
296
296
|
return [];
|
|
297
|
-
return store.
|
|
297
|
+
return store.listLeadTurnEvidence(turnId).map(one => one.artifact);
|
|
298
298
|
};
|
|
299
299
|
/** The turn ends failed: its drafts are deleted, its cost settled — the whole reservation when any of it is unknown. */
|
|
300
300
|
const fail = (failed, message, unknownSpend) => {
|
|
301
301
|
now = clock();
|
|
302
|
-
store.
|
|
303
|
-
store.
|
|
302
|
+
store.finalizeLeadTurn(turnId, started.generation, { state: "failed", settledMicrousd: settled, unknownSpend, tokensIn, tokensOut, failureReason: failed }, now);
|
|
303
|
+
store.dropLeadAsk(turnId);
|
|
304
304
|
return { ok: false, turn: turnId, failed, message, unknownSpend };
|
|
305
305
|
};
|
|
306
306
|
/** The turn stopped at its deadline: the notice is saved in the thread with whatever its completed tool calls proposed, in
|
|
307
307
|
* the same write that ends the turn, so a refresh, another device or a restart shows the same outcome. */
|
|
308
308
|
const timedOut = (unknownSpend) => {
|
|
309
309
|
now = clock();
|
|
310
|
-
const message =
|
|
311
|
-
const saved = store.
|
|
312
|
-
store.
|
|
310
|
+
const message = leadTimeoutNotice(proposals > 0, unknownSpend);
|
|
311
|
+
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);
|
|
312
|
+
store.dropLeadAsk(turnId);
|
|
313
313
|
if (!saved)
|
|
314
|
-
return { ok: false, turn: turnId, failed: "superseded", message:
|
|
314
|
+
return { ok: false, turn: turnId, failed: "superseded", message: LEAD_FAILURE_COPY.stopped, unknownSpend: false };
|
|
315
315
|
return { ok: false, turn: turnId, failed: "timeout", message, unknownSpend, saved: true };
|
|
316
316
|
};
|
|
317
317
|
const deadlineLeft = () => TURN_WALL_CLOCK_MS - (clock().getTime() - turnStartedAt);
|
|
318
318
|
/** The channel's standing, bounded by the turn's deadline: a lookup that never answers cannot hold the turn open. */
|
|
319
|
-
const revalidated = async () => input.revalidate === undefined ? { ok: true } : settleBy(input.revalidate(), deadlineLeft() +
|
|
319
|
+
const revalidated = async () => input.revalidate === undefined ? { ok: true } : settleBy(input.revalidate(), deadlineLeft() + LEAD_ABORT_GRACE_MS);
|
|
320
320
|
/** The turn's own row, re-read: still running under our generation, or someone ended it under us. */
|
|
321
321
|
const stillOurs = () => {
|
|
322
|
-
const row = store.
|
|
322
|
+
const row = store.getLeadTurn(turnId);
|
|
323
323
|
return row !== null && row.state === "running" && row.generation === started.generation;
|
|
324
324
|
};
|
|
325
325
|
// The channel lookup can await external state. Re-read all local authority
|
|
326
326
|
// AFTER it resolves, immediately before sending context or using a tool.
|
|
327
327
|
const guard = (channel) => {
|
|
328
328
|
if (!stillOurs())
|
|
329
|
-
return { ok: false, turn: turnId, failed: "superseded", message:
|
|
329
|
+
return { ok: false, turn: turnId, failed: "superseded", message: LEAD_FAILURE_COPY.stopped, unknownSpend: false };
|
|
330
330
|
if (channel === LATE)
|
|
331
331
|
return timedOut(false);
|
|
332
332
|
if (!channel.ok)
|
|
333
|
-
return fail("revoked",
|
|
333
|
+
return fail("revoked", LEAD_CHANNEL_COPY[channel.reason] ?? LEAD_CHANNEL_COPY["access-changed"], false);
|
|
334
334
|
const standing = reproveApprover(store, who);
|
|
335
|
-
const liveSession = store.
|
|
336
|
-
const liveThread = store.
|
|
335
|
+
const liveSession = store.getLeadSession(session.id);
|
|
336
|
+
const liveThread = store.getLeadThread(thread.id);
|
|
337
337
|
if (!standing.ok || liveSession === null || liveSession.endedAt !== null || liveThread === null || liveThread.closedAt !== null) {
|
|
338
|
-
return fail("revoked",
|
|
338
|
+
return fail("revoked", LEAD_FAILURE_COPY.ended, false);
|
|
339
339
|
}
|
|
340
340
|
return null;
|
|
341
341
|
};
|
|
@@ -351,7 +351,7 @@ export async function runMateTurn(input) {
|
|
|
351
351
|
const replyLimit = input.channel === undefined ? null : LEAD_REPLY_LIMITS[input.channel];
|
|
352
352
|
const unbacked = (text) => {
|
|
353
353
|
const claim = deliverableClaim(text);
|
|
354
|
-
if (claim === null || shownControl || replyCarriesDeliverable(text) || store.
|
|
354
|
+
if (claim === null || shownControl || replyCarriesDeliverable(text) || store.listLeadTurnEvidence(turnId).length > 0)
|
|
355
355
|
return null;
|
|
356
356
|
return claim;
|
|
357
357
|
};
|
|
@@ -367,42 +367,42 @@ export async function runMateTurn(input) {
|
|
|
367
367
|
// Tool results join the outbound body: scanned again before every dispatch.
|
|
368
368
|
const outbound = typeof request === "string" ? request : request.body;
|
|
369
369
|
if (scanForSecrets(outbound).length > 0) {
|
|
370
|
-
return fail("secret-refused",
|
|
370
|
+
return fail("secret-refused", LEAD_FAILURE_COPY.secretRead, false);
|
|
371
371
|
}
|
|
372
|
-
const step = store.
|
|
372
|
+
const step = store.openLeadStep({ leadTurn: turnId, generation: started.generation, approver: who.name, credentialKey, provider: config.provider, model: config.model, deadlineMs: remainingMs + 10_000 }, now);
|
|
373
373
|
if (!step.ok) {
|
|
374
374
|
if (step.reason === "latched")
|
|
375
|
-
return fail("latched",
|
|
376
|
-
return { ok: false, turn: turnId, failed: "superseded", message:
|
|
375
|
+
return fail("latched", LEAD_FAILURE_COPY.paused, false);
|
|
376
|
+
return { ok: false, turn: turnId, failed: "superseded", message: LEAD_FAILURE_COPY.stopped, unknownSpend: false };
|
|
377
377
|
}
|
|
378
378
|
const stepStarted = store.startChatTurn(step.id, now);
|
|
379
379
|
if (!stepStarted.ok)
|
|
380
|
-
return fail("provider-error",
|
|
380
|
+
return fail("provider-error", LEAD_FAILURE_COPY.notStarted, false);
|
|
381
381
|
steps++;
|
|
382
382
|
progress({ kind: "step", turn: turnId, step: steps });
|
|
383
383
|
const requestBytes = Buffer.byteLength(outbound, "utf8");
|
|
384
384
|
let result;
|
|
385
385
|
// The turn owns its deadline (lead-stall, release check 2438): at the deadline the request is aborted, which ends a
|
|
386
|
-
// harness's whole process group, and a provider that still has not settled
|
|
386
|
+
// harness's whole process group, and a provider that still has not settled LEAD_ABORT_GRACE_MS later is left behind.
|
|
387
387
|
// Whatever it says after that is never read; the turn has already ended with its notice.
|
|
388
388
|
const controller = new AbortController();
|
|
389
389
|
const abortAt = setTimeout(() => controller.abort(), remainingMs);
|
|
390
|
-
const settle = (work) => settleBy(work, remainingMs +
|
|
390
|
+
const settle = (work) => settleBy(work, remainingMs + LEAD_ABORT_GRACE_MS)
|
|
391
391
|
.then(value => value === LATE || controller.signal.aborted ? { ok: false, problem: "timeout" } : value)
|
|
392
392
|
.finally(() => clearTimeout(abortAt));
|
|
393
393
|
if (direct) {
|
|
394
|
-
result = await settle(
|
|
394
|
+
result = await settle(performLeadRequest(request, directProvider, controller.signal, fetcher)
|
|
395
395
|
.catch(() => ({ ok: false, problem: "network" })));
|
|
396
396
|
}
|
|
397
397
|
else {
|
|
398
|
-
const runner = input.subscriptionRunner ??
|
|
398
|
+
const runner = input.subscriptionRunner ?? performSubscriptionLeadRequest;
|
|
399
399
|
result = await settle((async () => runner({
|
|
400
400
|
provider: subscriptionProvider,
|
|
401
401
|
model: config.model,
|
|
402
|
-
system:
|
|
402
|
+
system: LEAD_CONTRACT,
|
|
403
403
|
dataDocument: document,
|
|
404
404
|
history,
|
|
405
|
-
tools:
|
|
405
|
+
tools: LEAD_TOOL_SCHEMAS,
|
|
406
406
|
timeoutMs: remainingMs,
|
|
407
407
|
signal: controller.signal,
|
|
408
408
|
// The reply as it is written; text that looks like a secret is
|
|
@@ -417,12 +417,12 @@ export async function runMateTurn(input) {
|
|
|
417
417
|
if (!direct) {
|
|
418
418
|
finishStep({ state: "failed", failureReason: result.problem === "timeout" ? "timeout" : result.problem === "malformed-reply" ? "malformed-reply" : "provider-error", settledMicrousd: 0 });
|
|
419
419
|
const message = result.problem === "not-found"
|
|
420
|
-
?
|
|
420
|
+
? LEAD_FAILURE_COPY.notInstalled
|
|
421
421
|
: result.problem === "timeout"
|
|
422
|
-
?
|
|
422
|
+
? LEAD_FAILURE_COPY.tooLong
|
|
423
423
|
: result.problem === "malformed-reply"
|
|
424
|
-
?
|
|
425
|
-
:
|
|
424
|
+
? LEAD_FAILURE_COPY.unreadable
|
|
425
|
+
: LEAD_FAILURE_COPY.signIn;
|
|
426
426
|
if (result.problem === "timeout")
|
|
427
427
|
return timedOut(false);
|
|
428
428
|
return fail(result.problem === "malformed-reply" ? "malformed-reply" : "provider-error", message, false);
|
|
@@ -430,7 +430,7 @@ export async function runMateTurn(input) {
|
|
|
430
430
|
if (result.problem.startsWith("status-")) {
|
|
431
431
|
// The provider ANSWERED with an error: nothing billed for this step.
|
|
432
432
|
finishStep({ state: "failed", failureReason: "provider-error", settledMicrousd: 0 });
|
|
433
|
-
return fail("provider-error",
|
|
433
|
+
return fail("provider-error", LEAD_FAILURE_COPY.refused, false);
|
|
434
434
|
}
|
|
435
435
|
if (result.problem === "timeout") {
|
|
436
436
|
finishStep({ state: "failed", failureReason: "timeout", settledMicrousd: null, unknownSpend: true });
|
|
@@ -438,17 +438,17 @@ export async function runMateTurn(input) {
|
|
|
438
438
|
}
|
|
439
439
|
if (result.problem === "network") {
|
|
440
440
|
finishStep({ state: "failed", failureReason: "provider-error", settledMicrousd: null, unknownSpend: true });
|
|
441
|
-
return fail("provider-error",
|
|
441
|
+
return fail("provider-error", LEAD_FAILURE_COPY.lostUnknownCost, true);
|
|
442
442
|
}
|
|
443
443
|
finishStep({ state: "failed", failureReason: "malformed-reply", settledMicrousd: null, unknownSpend: true });
|
|
444
|
-
return fail("malformed-reply",
|
|
444
|
+
return fail("malformed-reply", LEAD_FAILURE_COPY.unreadableUnknownCost, true);
|
|
445
445
|
}
|
|
446
446
|
const answer = result.answer;
|
|
447
447
|
// Usage that cannot be true — more input tokens than bytes sent — is a
|
|
448
448
|
// malformed reply with unknown cost, never a number to settle by.
|
|
449
449
|
if (direct && answer.tokensIn > requestBytes) {
|
|
450
450
|
finishStep({ state: "failed", failureReason: "malformed-reply", settledMicrousd: null, unknownSpend: true });
|
|
451
|
-
return fail("malformed-reply",
|
|
451
|
+
return fail("malformed-reply", LEAD_FAILURE_COPY.unreadableUnknownCost, true);
|
|
452
452
|
}
|
|
453
453
|
// The pinned math, or the provider's own reported charge when HIGHER.
|
|
454
454
|
const stepSettled = direct
|
|
@@ -467,7 +467,7 @@ export async function runMateTurn(input) {
|
|
|
467
467
|
return changed;
|
|
468
468
|
if (answer.calls.length === 0) {
|
|
469
469
|
if (answer.text.trim() === "")
|
|
470
|
-
return fail("malformed-reply",
|
|
470
|
+
return fail("malformed-reply", LEAD_FAILURE_COPY.empty, false);
|
|
471
471
|
// "Here's the screenshot" with nothing attached or linked: one repair step to attach it or drop the claim; never the claim alone.
|
|
472
472
|
const claim = unbacked(answer.text);
|
|
473
473
|
if (claim !== null && !repaired && steps < maxSteps) {
|
|
@@ -486,11 +486,11 @@ export async function runMateTurn(input) {
|
|
|
486
486
|
reply = candidate;
|
|
487
487
|
break;
|
|
488
488
|
}
|
|
489
|
-
if (answer.calls.length >
|
|
490
|
-
return fail("malformed-reply",
|
|
489
|
+
if (answer.calls.length > LEAD_MAX_CALLS_PER_STEP)
|
|
490
|
+
return fail("malformed-reply", LEAD_FAILURE_COPY.unusable, false);
|
|
491
491
|
for (const call of answer.calls) {
|
|
492
|
-
if (!
|
|
493
|
-
return fail("malformed-reply",
|
|
492
|
+
if (!isLeadTool(call.name))
|
|
493
|
+
return fail("malformed-reply", LEAD_FAILURE_COPY.unusable, false);
|
|
494
494
|
}
|
|
495
495
|
history.push({ role: "assistant", text: answer.text, calls: answer.calls });
|
|
496
496
|
for (const [index, call] of answer.calls.entries()) {
|
|
@@ -501,13 +501,13 @@ export async function runMateTurn(input) {
|
|
|
501
501
|
}
|
|
502
502
|
// Position within this turn is unique even if a provider reuses call ids.
|
|
503
503
|
const progressId = `${turnId}:${steps}:${index}`;
|
|
504
|
-
progress({ kind: "tool", turn: turnId, step: steps, id: progressId, label:
|
|
505
|
-
const outcome =
|
|
504
|
+
progress({ kind: "tool", turn: turnId, step: steps, id: progressId, label: leadToolLabel(call.name) });
|
|
505
|
+
const outcome = executeLeadTool({ store, who, now: clock(), draft, selectEvidence, step: steps, readDecisions, readResults, searchedMemory, checkedCapabilities, ask, ...(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 }) }, call.name, call.args, view);
|
|
506
506
|
let displayOutcome = { state: "succeeded" };
|
|
507
507
|
if (!outcome.ok) {
|
|
508
|
-
const reason =
|
|
508
|
+
const reason = leadToolFailureReason(call.name, outcome.message);
|
|
509
509
|
displayOutcome = { state: "failed", reason: scanForSecrets(reason).length === 0
|
|
510
|
-
? reason : `That step didn't work (${
|
|
510
|
+
? reason : `That step didn't work (${leadToolLabel(call.name)}).` };
|
|
511
511
|
}
|
|
512
512
|
progress({ kind: "tool-result", turn: turnId, step: steps, id: progressId, outcome: displayOutcome });
|
|
513
513
|
if (READ_TOOLS.has(call.name))
|
|
@@ -516,7 +516,7 @@ export async function runMateTurn(input) {
|
|
|
516
516
|
shownControl = true;
|
|
517
517
|
// Opt-in, local diagnostics for end-to-end runs: what the lead asked of each tool and what came back (its start), keys
|
|
518
518
|
// blanked. Each line stays whole JSON so a run can assert on what a tool returned rather than on the model's words.
|
|
519
|
-
if (envValue(process.env, "MATE_TRACE") === "1") {
|
|
519
|
+
if (envValue(process.env, "LEAD_TRACE") === "1" || envValue(process.env, "MATE_TRACE") === "1") { // MATE_TRACE: the name before D5
|
|
520
520
|
const blanked = (_, value) => typeof value === "string" ? redactSecretAssignments(redactSecretLines(value, scanForSecrets(value))) : value;
|
|
521
521
|
const args = JSON.stringify(call.args, blanked).slice(0, 1500);
|
|
522
522
|
const result = JSON.stringify(outcome.ok ? outcome.body : null, blanked)?.slice(0, 2000) ?? null;
|
|
@@ -528,16 +528,16 @@ export async function runMateTurn(input) {
|
|
|
528
528
|
if (reply === null) {
|
|
529
529
|
stoppedAtCap = true;
|
|
530
530
|
const last = lastText.trim() === "" || unbacked(lastText) === null ? lastText.trim() : dropDeliverableClaims(lastText);
|
|
531
|
-
reply = `${last === "" ? "" : `${last}\n\n`}(I stopped here: this answer needed more steps than ${maxSteps <
|
|
531
|
+
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.)`;
|
|
532
532
|
}
|
|
533
533
|
// Ruling 11: model text is scanned before it becomes durable.
|
|
534
534
|
if (scanForSecrets(reply).length > 0)
|
|
535
|
-
return fail("secret-refused",
|
|
535
|
+
return fail("secret-refused", LEAD_FAILURE_COPY.secretReply, false);
|
|
536
536
|
now = clock();
|
|
537
537
|
const activity = activitySummary(reads, proposals, steps);
|
|
538
538
|
// One write (finding 12): settle, debit, promote the drafts, append the assistant text.
|
|
539
|
-
const finalized = store.
|
|
539
|
+
const finalized = store.finalizeLeadTurn(turnId, started.generation, { state: "answered", settledMicrousd: settled, tokensIn, tokensOut, message: { text: reply, activity } }, now);
|
|
540
540
|
if (!finalized)
|
|
541
|
-
return { ok: false, turn: turnId, failed: "superseded", message:
|
|
541
|
+
return { ok: false, turn: turnId, failed: "superseded", message: LEAD_FAILURE_COPY.stopped, unknownSpend: false };
|
|
542
542
|
return { ok: true, turn: turnId, reply, activity, proposals, steps, stoppedAtCap, settledMicrousd: settled };
|
|
543
543
|
}
|