@intentic/sandbox-contract 1.295.0 → 1.296.0
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/dist/contracts/agent.contract.d.ts +23 -6
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.js +10 -0
- package/dist/contracts/agent.contract.js.map +1 -1
- package/dist/contracts/agents.contract.d.ts +1 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +95 -95
- package/dist/contracts/sessions.contract.d.ts +1 -0
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +4 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +1 -0
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/events/agent-events.d.ts +3 -0
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/transcript.d.ts +6 -0
- package/dist/events/transcript.d.ts.map +1 -1
- package/dist/events/transcript.js +1 -1
- package/dist/events/transcript.js.map +1 -1
- package/dist/index.d.ts +32 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/models/model-offer.d.ts +32 -0
- package/dist/models/model-offer.d.ts.map +1 -0
- package/dist/models/model-offer.js +81 -0
- package/dist/models/model-offer.js.map +1 -0
- package/dist/models/model-roles.d.ts +7 -0
- package/dist/models/model-roles.d.ts.map +1 -1
- package/dist/models/model-roles.js +7 -0
- package/dist/models/model-roles.js.map +1 -1
- package/dist/schemas/agent.d.ts +2 -0
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +4 -0
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/model-route.d.ts +27 -0
- package/dist/schemas/model-route.d.ts.map +1 -0
- package/dist/schemas/model-route.js +30 -0
- package/dist/schemas/model-route.js.map +1 -0
- package/dist/schemas/providers/usage.d.ts +1 -0
- package/dist/schemas/providers/usage.d.ts.map +1 -1
- package/dist/schemas/providers/usage.js +1 -0
- package/dist/schemas/providers/usage.js.map +1 -1
- package/dist/schemas/settings.d.ts +2 -0
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +2 -2
- package/dist/schemas/settings.js.map +1 -1
- package/dist/state/definition.d.ts +8 -8
- package/package.json +5 -5
- package/src/contracts/agent.contract.ts +12 -0
- package/src/events/transcript.ts +1 -1
- package/src/index.ts +2 -0
- package/src/models/model-offer.test.ts +88 -0
- package/src/models/model-offer.ts +151 -0
- package/src/models/model-roles.ts +9 -0
- package/src/schemas/agent.ts +7 -0
- package/src/schemas/model-route.ts +43 -0
- package/src/schemas/providers/usage.ts +4 -0
- package/src/schemas/settings.ts +6 -2
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { modelPinKey } from "./model-pins.js";
|
|
2
|
+
import type { AgentProvider } from "../schemas/agent.js";
|
|
3
|
+
import type { ModelPick } from "../schemas/model-route.js";
|
|
4
|
+
|
|
5
|
+
// What the Auto judge is allowed to choose from, and how its reply is read back. Pure: the daemon gathers the facts
|
|
6
|
+
// (catalogs, connected accounts, headroom readings) and this turns them into the lines the model sees and the
|
|
7
|
+
// validator that reads its answer. Nothing outside the offer can be chosen, so a hallucinated model id is caught here
|
|
8
|
+
// rather than at the provider.
|
|
9
|
+
|
|
10
|
+
export interface OfferedWindow {
|
|
11
|
+
// Narrow token for the pool's length, e.g. "5h", "wk"; absent when nothing names how long the window runs.
|
|
12
|
+
readonly short?: string;
|
|
13
|
+
readonly label?: string;
|
|
14
|
+
// 0-100 REMAINING, the complement of the reading's utilization: the judge is asked about what is left, not spent.
|
|
15
|
+
readonly left: number;
|
|
16
|
+
// Epoch seconds, as every reset instant on this wire.
|
|
17
|
+
readonly resetsAt?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface OfferedAccount {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
// The owner's own name for it where there is one; the id is what must be replied with either way.
|
|
23
|
+
readonly label?: string;
|
|
24
|
+
// Empty means never measured, which is not the same as measured full and must not read as it.
|
|
25
|
+
readonly windows: readonly OfferedWindow[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface OfferedModel {
|
|
29
|
+
readonly provider: AgentProvider;
|
|
30
|
+
readonly model: string;
|
|
31
|
+
readonly label: string;
|
|
32
|
+
// Weakest first, as the catalog row published them; empty means this model takes no effort setting.
|
|
33
|
+
readonly efforts: readonly string[];
|
|
34
|
+
// One short clause about what the model is for, where the catalog carries one.
|
|
35
|
+
readonly note?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ModelOffer {
|
|
39
|
+
// Already filtered: a model whose every account is at cap never reaches here.
|
|
40
|
+
readonly models: readonly OfferedModel[];
|
|
41
|
+
// Accounts that can pay, per provider id. A provider with none still offers its models: an unnamed account is a
|
|
42
|
+
// real state (a plain key, an endpoint), and the turn resolves one itself.
|
|
43
|
+
readonly accounts: Readonly<Record<string, readonly OfferedAccount[]>>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Relative phrasing of a reset, since an absolute instant would need the reader's timezone; deliberately coarse at
|
|
47
|
+
// every scale. `reopensAt` is epoch seconds, `now` epoch ms.
|
|
48
|
+
export const renewsInWords = (reopensAt: number, now: number): string => {
|
|
49
|
+
const seconds = reopensAt - Math.floor(now / 1000);
|
|
50
|
+
if (seconds <= 60) {
|
|
51
|
+
return `any moment`;
|
|
52
|
+
}
|
|
53
|
+
if (seconds < 60 * 60) {
|
|
54
|
+
return `in about ${Math.round(seconds / 60)} min`;
|
|
55
|
+
}
|
|
56
|
+
if (seconds < 36 * 60 * 60) {
|
|
57
|
+
return `in about ${Math.round(seconds / 3600)}h`;
|
|
58
|
+
}
|
|
59
|
+
return `in about ${Math.round(seconds / 86_400)} days`;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const windowWords = (window: OfferedWindow, now: number): string => {
|
|
63
|
+
const name = window.short ?? window.label ?? `allowance`;
|
|
64
|
+
const renews = window.resetsAt === undefined ? `` : ` (renews ${renewsInWords(window.resetsAt, now)})`;
|
|
65
|
+
return `${name}: ${Math.round(window.left)}% left${renews}`;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// An account with no reading says so rather than being left off: "not measured" is a fact the judge should weigh,
|
|
69
|
+
// and an account silently missing from the list would read as one that cannot pay.
|
|
70
|
+
const accountLine = (account: OfferedAccount, now: number): string => {
|
|
71
|
+
const name = account.label === undefined || account.label === account.id ? account.id : `${account.id} (${account.label})`;
|
|
72
|
+
const state = account.windows.length === 0 ? `allowance never measured` : account.windows.map((window) => windowWords(window, now)).join(`, `);
|
|
73
|
+
return `- ${name} — ${state}`;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const modelLine = (model: OfferedModel): string => {
|
|
77
|
+
const efforts = model.efforts.length === 0 ? `` : ` Effort: ${model.efforts.join(`, `)}.`;
|
|
78
|
+
const note = model.note === undefined ? `` : ` ${model.note.trim().replace(/\.$/u, ``)}.`;
|
|
79
|
+
return `- ${modelPinKey(model)} — ${model.label}.${note}${efforts}`;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// The two blocks the prompt shows: what may be run, and who can pay for it. Providers keep the offer's own order, so a
|
|
83
|
+
// reply can be compared against the list a person would read.
|
|
84
|
+
export const offerLines = (offer: ModelOffer, now: number): readonly string[] => {
|
|
85
|
+
const providers = [...new Set(offer.models.map((model) => model.provider))];
|
|
86
|
+
return [
|
|
87
|
+
`Models you may choose:`,
|
|
88
|
+
...offer.models.map(modelLine),
|
|
89
|
+
``,
|
|
90
|
+
`Accounts that can pay, and how much allowance each has left:`,
|
|
91
|
+
...providers.flatMap((provider) => {
|
|
92
|
+
const accounts = offer.accounts[provider] ?? [];
|
|
93
|
+
return accounts.length === 0
|
|
94
|
+
? [`- ${provider}: no named account; this provider resolves its own.`]
|
|
95
|
+
: [`- ${provider}:`, ...accounts.map((account) => ` ${accountLine(account, now)}`)];
|
|
96
|
+
}),
|
|
97
|
+
];
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Wrapper words a model reaches for anyway: a fence, a bullet, quotes, a trailing period.
|
|
101
|
+
const FENCE_LINE = /^```/u;
|
|
102
|
+
const BULLET_LINE = /^[-*•]\s+/u;
|
|
103
|
+
const FIELD = /^(model|effort|account)\s*:\s*(.+)$/iu;
|
|
104
|
+
|
|
105
|
+
const bare = (value: string): string => value.trim().replace(/^[`'"]+/u, ``).replace(/[.`'"]+$/u, ``).trim();
|
|
106
|
+
|
|
107
|
+
// The reply's `key: value` lines, lowercased keys, bullets and fences stripped. First wins: a model that answers twice
|
|
108
|
+
// meant its first answer, and a later line is commentary on it.
|
|
109
|
+
const fieldsOf = (reply: string): ReadonlyMap<string, string> => {
|
|
110
|
+
const fields = new Map<string, string>();
|
|
111
|
+
for (const line of reply.split(`\n`)) {
|
|
112
|
+
const trimmed = line.trim().replace(BULLET_LINE, ``);
|
|
113
|
+
const found = FENCE_LINE.test(trimmed) ? null : FIELD.exec(trimmed);
|
|
114
|
+
const key = found?.[1]?.toLowerCase();
|
|
115
|
+
if (key !== undefined && found?.[2] !== undefined && !fields.has(key)) {
|
|
116
|
+
fields.set(key, bare(found[2]));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return fields;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export interface ParsedPick {
|
|
123
|
+
readonly pick: ModelPick | undefined;
|
|
124
|
+
// The reply's literal model line, for the sentence explaining why a rung was stepped over.
|
|
125
|
+
readonly token: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Reads the keyed reply (`model:` / `effort:` / `account:`) against the offer. The MODEL is the load-bearing choice:
|
|
129
|
+
// one that names nothing offered yields no pick, and the caller steps to the next rung rather than running an id no
|
|
130
|
+
// provider has. An unrecognised effort or account is dropped instead, keeping the model it came with — those are
|
|
131
|
+
// refinements, and the turn's own defaults answer for them correctly.
|
|
132
|
+
export const parseModelPick = (reply: string, offer: ModelOffer): ParsedPick => {
|
|
133
|
+
const fields = fieldsOf(reply);
|
|
134
|
+
const token = fields.get(`model`) ?? ``;
|
|
135
|
+
const chosen = offer.models.find((model) => modelPinKey(model).toLowerCase() === token.toLowerCase());
|
|
136
|
+
if (chosen === undefined) {
|
|
137
|
+
return { pick: undefined, token };
|
|
138
|
+
}
|
|
139
|
+
const effort = fields.get(`effort`);
|
|
140
|
+
const account = fields.get(`account`);
|
|
141
|
+
const named = (offer.accounts[chosen.provider] ?? []).find((entry) => entry.id.toLowerCase() === account?.toLowerCase());
|
|
142
|
+
return {
|
|
143
|
+
pick: {
|
|
144
|
+
provider: chosen.provider,
|
|
145
|
+
model: chosen.model,
|
|
146
|
+
...(effort !== undefined && chosen.efforts.includes(effort.toLowerCase()) ? { effort: effort.toLowerCase() } : {}),
|
|
147
|
+
...(named === undefined ? {} : { account: named.id }),
|
|
148
|
+
},
|
|
149
|
+
token,
|
|
150
|
+
};
|
|
151
|
+
};
|
|
@@ -65,6 +65,15 @@ export const MODEL_ROLES = [
|
|
|
65
65
|
kind: "helper",
|
|
66
66
|
icon: "users",
|
|
67
67
|
},
|
|
68
|
+
{
|
|
69
|
+
// Picks one offered `provider:model` line, an effort and an account from a fixed list; a classification, not
|
|
70
|
+
// free text. Prompt carries live account headroom, so it must not be a model whose own allowance it is reading.
|
|
71
|
+
id: "model-router",
|
|
72
|
+
label: "Auto model choice",
|
|
73
|
+
blurb: "Which model reads a new chat's first message and picks the model, effort and account it runs on.",
|
|
74
|
+
kind: "helper",
|
|
75
|
+
icon: "sparkles",
|
|
76
|
+
},
|
|
68
77
|
{
|
|
69
78
|
id: "pipeline-fix",
|
|
70
79
|
label: "Pipeline fixes",
|
package/src/schemas/agent.ts
CHANGED
|
@@ -262,6 +262,13 @@ export const AgentTurnSchema = z
|
|
|
262
262
|
),
|
|
263
263
|
// Vetoes automatic tier selection for this turn; the judge still runs and records its verdict, but nothing is
|
|
264
264
|
// substituted.
|
|
265
|
+
// Set by the composer on the one turn whose model the Auto judge chose; the daemon only records it.
|
|
266
|
+
autoPicked: z
|
|
267
|
+
.boolean()
|
|
268
|
+
.optional()
|
|
269
|
+
.describe(
|
|
270
|
+
"Whether this turn's model was chosen for you by reading the conversation's opening message, rather than picked by hand. Recorded so the choice can be judged later against what you did next.",
|
|
271
|
+
),
|
|
265
272
|
tierHold: z
|
|
266
273
|
.boolean()
|
|
267
274
|
.optional()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AgentProviderSchema } from "./agent.js";
|
|
3
|
+
|
|
4
|
+
// Asked once per chat, on the message it opens with; answers with the model, effort and account that whole
|
|
5
|
+
// conversation runs on. Distinct from prompt-complexity.ts, which judges a turn at a time and may only ever name a
|
|
6
|
+
// cheaper rung of the provider already picked: this one chooses freely, and only before anything has run.
|
|
7
|
+
|
|
8
|
+
export const ModelRouteAskSchema = z.object({
|
|
9
|
+
prompt: z.string().min(1).max(20000).describe("The message a new chat is about to open with."),
|
|
10
|
+
paths: z
|
|
11
|
+
.array(z.string().min(1).max(500))
|
|
12
|
+
.max(50)
|
|
13
|
+
.default([])
|
|
14
|
+
.describe("Workspace paths the message names: uploads, @-mentions, the editor's own file. How much real code the work touches."),
|
|
15
|
+
editorContext: z.boolean().optional().describe("Whether the message carries a file and selection the user pointed at, so it is about real code."),
|
|
16
|
+
planMode: z.boolean().optional().describe("Whether the chat opens in plan mode, which is a request to think before acting."),
|
|
17
|
+
});
|
|
18
|
+
export type ModelRouteAsk = z.infer<typeof ModelRouteAskSchema>;
|
|
19
|
+
|
|
20
|
+
export const ModelPickSchema = z.object({
|
|
21
|
+
provider: AgentProviderSchema.describe("Which provider serves the conversation."),
|
|
22
|
+
model: z.string().min(1).describe("Which of its models."),
|
|
23
|
+
effort: z.string().optional().describe("How hard it should think, where the model offers a choice. Absent takes the model's own default."),
|
|
24
|
+
account: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("Which connected account pays, by its daemon-minted id. Absent leaves it to whichever account has the most headroom."),
|
|
28
|
+
});
|
|
29
|
+
export type ModelPick = z.infer<typeof ModelPickSchema>;
|
|
30
|
+
|
|
31
|
+
export const ModelRouteSchema = z.object({
|
|
32
|
+
// Absent is a real, safe answer: the chat runs on whatever the picker remembered, and says why nothing moved.
|
|
33
|
+
pick: ModelPickSchema.optional().describe("What the conversation should run on, or absent when nothing could be chosen and the usual pick stands."),
|
|
34
|
+
reason: z.string().describe("Why, in the one line a chat can show. Present whether or not a model was named."),
|
|
35
|
+
// Absent means nothing was spent: the role was unset, every rung refused, or the deadline passed first.
|
|
36
|
+
judge: z
|
|
37
|
+
.string()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe(
|
|
40
|
+
"Which model answered, as `provider:model`, so the chat can name what the reading cost. Absent when no model was reached at all.",
|
|
41
|
+
),
|
|
42
|
+
});
|
|
43
|
+
export type ModelRoute = z.infer<typeof ModelRouteSchema>;
|
|
@@ -102,6 +102,10 @@ export const UsageTurnSchema = z.object({
|
|
|
102
102
|
tierCeiling: z.number().optional(),
|
|
103
103
|
// The turn carried AgentTurn.tierHold: the user vetoed a fast verdict. Absent means no veto.
|
|
104
104
|
tierDenied: z.boolean().optional(),
|
|
105
|
+
// This turn's model was chosen by the Auto judge reading the conversation's opening message, not picked by hand.
|
|
106
|
+
// Marked on that one turn only; a later row of the same conversation naming a different model is the user
|
|
107
|
+
// overruling it, which is the escalation rate this feature has to be able to answer for.
|
|
108
|
+
autoPicked: z.boolean().optional(),
|
|
105
109
|
});
|
|
106
110
|
export type UsageTurn = z.infer<typeof UsageTurnSchema>;
|
|
107
111
|
// Ledger grouped by day, provider, account, model, harness and conversation, one panel's worth of rows per active day.
|
package/src/schemas/settings.ts
CHANGED
|
@@ -317,11 +317,15 @@ export const SandboxSettingsSchema = z.object({
|
|
|
317
317
|
// off: the judge never runs.
|
|
318
318
|
// shadow (default): the judge scores every turn to the ledger; nothing is routed.
|
|
319
319
|
// on: a turn judged fast runs on the cheap rung, where the provider publishes one.
|
|
320
|
+
// judge: the deterministic scorer keeps writing shadow rows but routes nothing; the Auto picker row is offered
|
|
321
|
+
// instead, and a model reads a new chat's opening message once to choose what the whole conversation runs on.
|
|
322
|
+
// Exclusive with `on` because the two answer the same question at different moments, and a per-turn downgrade
|
|
323
|
+
// underneath a picked-by-model conversation would be overruling a choice already made about this chat.
|
|
320
324
|
autoTier: z
|
|
321
|
-
.enum(["off", "shadow", "on"])
|
|
325
|
+
.enum(["off", "shadow", "on", "judge"])
|
|
322
326
|
.default("shadow")
|
|
323
327
|
.describe(
|
|
324
|
-
"Whether an easy-looking turn may run on a cheaper model from the same provider. Three states rather than a switch, because the middle one is the only honest road to the third: it scores every turn and routes nothing, so the guess can become a measurement before it changes anything. It can only ever route down, so the worst case is one turn's quality rather than a bill nobody asked for.",
|
|
328
|
+
"Whether an easy-looking turn may run on a cheaper model from the same provider. Three states rather than a switch, because the middle one is the only honest road to the third: it scores every turn and routes nothing, so the guess can become a measurement before it changes anything. It can only ever route down, so the worst case is one turn's quality rather than a bill nobody asked for. The fourth, Auto, answers a different question: rather than downgrading turns one by one, it offers an Auto row in the model picker and has a model read a new chat's first message to choose what that whole conversation runs on.",
|
|
325
329
|
),
|
|
326
330
|
// `balanced` is what every verdict recorded before this setting existed was judged against, so shadow history stays
|
|
327
331
|
// comparable.
|