ravensight-playtest 0.7.0 → 0.7.5
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/README.md +61 -8
- package/package.json +3 -3
- package/src/auth/scopes.js +36 -0
- package/src/cli.js +42 -9
- package/src/commands/brief.js +1 -1
- package/src/commands/build.js +23 -1
- package/src/commands/check.js +5 -1
- package/src/commands/fakeCore.js +5 -1
- package/src/commands/fix.js +2 -0
- package/src/commands/hosted.js +144 -2
- package/src/commands/profile.js +2 -1
- package/src/commands/run.js +86 -5
- package/src/hosted/buildStatus.js +35 -0
- package/src/hosted/buildUpload.js +24 -2
- package/src/hosted/recordedRun.js +135 -3
- package/src/hosted/status.js +108 -0
- package/src/run/aggregate.js +69 -10
- package/src/run/budgetReserve.js +102 -0
- package/src/run/context.js +122 -3
- package/src/run/drivers/web.js +342 -126
- package/src/run/failureReason.js +77 -0
- package/src/run/gameRefusal.js +57 -0
- package/src/run/model.js +18 -1
- package/src/run/openPr.js +8 -1
- package/src/run/personaLoop.js +333 -76
- package/src/run/regenerate.js +4 -1
- package/src/run/report.js +54 -0
- package/src/run/tools.js +31 -3
- package/src/run/usage.js +54 -0
- package/src/ui/index.js +21 -6
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { OUTPUT_WEIGHT } from './usage.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Keeping enough of a run's model budget back to write its report.
|
|
5
|
+
*
|
|
6
|
+
* The proxy reserves each call's pessimistic projection before sending it:
|
|
7
|
+
* the whole request body at the full input rate (characters / 3.5) plus the
|
|
8
|
+
* output ceiling at the output rate. A call whose projection does not fit
|
|
9
|
+
* what is left of the run's budget is refused with 402 before anything is
|
|
10
|
+
* sent. The server's own warning comes at 80 percent of the budget, counting
|
|
11
|
+
* the call being made, and in the October 26 runs it never came: one act
|
|
12
|
+
* turn's projection jumped the run from under 80 percent to a refusal, and the
|
|
13
|
+
* report that followed did not fit either.
|
|
14
|
+
*
|
|
15
|
+
* So before every act turn the loop asks this module whether, after that
|
|
16
|
+
* turn, the report call would still be admitted. Both projections are sized
|
|
17
|
+
* from the requests themselves (the act turn about to be sent and the report
|
|
18
|
+
* prompt as it would be built now), in input-token units, and priced at what
|
|
19
|
+
* one unit has actually cost this run (the server's spend over the ledger's
|
|
20
|
+
* weighted tokens, ./usage.js). No price is held here.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Characters per input token, the proxy's own projection rule. */
|
|
24
|
+
export const CHARS_PER_INPUT_TOKEN = 3.5;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Headroom on the report's projection: the prompt grows by the notes and
|
|
28
|
+
* observations of the turn still to be played, and a persona that escalates
|
|
29
|
+
* may write it on a dearer tier than the one the unit price was learned on.
|
|
30
|
+
*/
|
|
31
|
+
export const REPORT_RESERVE_MARGIN = 1.3;
|
|
32
|
+
|
|
33
|
+
/** Headroom on what the next act turn will really cost, over the dearest recent one. */
|
|
34
|
+
export const ACT_COST_MARGIN = 1.5;
|
|
35
|
+
|
|
36
|
+
/** A flat share of the run's budget never planned against. */
|
|
37
|
+
export const FLAT_MARGIN_RATIO = 0.03;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The proxy's projection of a request, in input-token units.
|
|
41
|
+
*
|
|
42
|
+
* @param {number} bodyChars the request's JSON length
|
|
43
|
+
* @param {number} maxTokens its output ceiling
|
|
44
|
+
* @returns {number}
|
|
45
|
+
*/
|
|
46
|
+
export function projectionUnits(bodyChars, maxTokens) {
|
|
47
|
+
return Math.max(0, Number(bodyChars) || 0) / CHARS_PER_INPUT_TOKEN + Math.max(0, Number(maxTokens) || 0) * OUTPUT_WEIGHT;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether play has to stop now so the report still fits.
|
|
52
|
+
*
|
|
53
|
+
* @param {Object} args
|
|
54
|
+
* @param {number|null} args.usdMax the run's model budget, from register
|
|
55
|
+
* @param {{usd: number, usdPerUnit: number}|null} args.spend the ledger's estimate
|
|
56
|
+
* @param {number} args.actChars the next act request's JSON length
|
|
57
|
+
* @param {number} args.actMaxTokens
|
|
58
|
+
* @param {number} args.actCostUnits what an act turn has really cost, at most, lately
|
|
59
|
+
* @param {number} args.reportChars the report request's JSON length if it were sent now
|
|
60
|
+
* @param {number} args.reportMaxTokens
|
|
61
|
+
* @returns {{stop: boolean, known: boolean, figures: Object}}
|
|
62
|
+
*/
|
|
63
|
+
export function reserveDecision({ usdMax, spend, actChars, actMaxTokens, actCostUnits, reportChars, reportMaxTokens }) {
|
|
64
|
+
const max = Number(usdMax);
|
|
65
|
+
if (!Number.isFinite(max) || max <= 0 || !spend || !Number.isFinite(spend.usdPerUnit) || spend.usdPerUnit <= 0) {
|
|
66
|
+
return { stop: false, known: false, figures: {} };
|
|
67
|
+
}
|
|
68
|
+
const price = spend.usdPerUnit;
|
|
69
|
+
const actProjected = price * projectionUnits(actChars, actMaxTokens);
|
|
70
|
+
const actCost = price * Math.max(0, Number(actCostUnits) || 0) * ACT_COST_MARGIN;
|
|
71
|
+
const reportProjected = price * projectionUnits(reportChars, reportMaxTokens) * REPORT_RESERVE_MARGIN;
|
|
72
|
+
const flat = max * FLAT_MARGIN_RATIO;
|
|
73
|
+
// The act turn itself has to be admitted, and after it the report has to be.
|
|
74
|
+
const actFits = spend.usd + actProjected <= max;
|
|
75
|
+
const reportFitsAfter = spend.usd + actCost + reportProjected + flat <= max;
|
|
76
|
+
const round = value => Math.round(value * 1e6) / 1e6;
|
|
77
|
+
return {
|
|
78
|
+
stop: !actFits || !reportFitsAfter,
|
|
79
|
+
known: true,
|
|
80
|
+
figures: {
|
|
81
|
+
usd_max: round(max),
|
|
82
|
+
usd_spent: round(spend.usd),
|
|
83
|
+
act_projected_usd: round(actProjected),
|
|
84
|
+
act_cost_usd: round(actCost),
|
|
85
|
+
report_reserve_usd: round(reportProjected),
|
|
86
|
+
margin_usd: round(flat)
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A registered run's model budget in USD: `limits.model_usd`, or the older
|
|
93
|
+
* `budget_usd` beside it. Null when the server sent neither, which leaves the
|
|
94
|
+
* reserve off and the server's own warning in charge.
|
|
95
|
+
*
|
|
96
|
+
* @param {Object} entry a run from the register answer
|
|
97
|
+
* @returns {number|null}
|
|
98
|
+
*/
|
|
99
|
+
export function runModelBudgetUsd(entry) {
|
|
100
|
+
const value = Number(entry && ((entry.limits && entry.limits.model_usd) ?? entry.budget_usd));
|
|
101
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
102
|
+
}
|
package/src/run/context.js
CHANGED
|
@@ -165,17 +165,136 @@ export function pruneObservations(messages, keep = KEEP_OBSERVATIONS) {
|
|
|
165
165
|
return out;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
/** What a tool_result with nothing in it is sent as. */
|
|
169
|
+
export const EMPTY_TOOL_RESULT_TEXT = '(no output)';
|
|
170
|
+
|
|
171
|
+
/** The tool_result for a tool_use the runner never answered. */
|
|
172
|
+
export const MISSING_TOOL_RESULT_TEXT = 'Not run: the runner did not play this call.';
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Make every tool_result in the conversation one the Messages API accepts.
|
|
176
|
+
*
|
|
177
|
+
* The rules the API enforces, and what each repair is:
|
|
178
|
+
*
|
|
179
|
+
* - **An `is_error` result is text only.** A result flagged `is_error` that
|
|
180
|
+
* also carries an image block is refused with a 400. That is the 0.7.2
|
|
181
|
+
* failure: a refused off-site `navigate` on a canvas build came back as
|
|
182
|
+
* "That did not work: ..." plus the screenshot, flagged as an error, and
|
|
183
|
+
* every persona of the job died on its next turn. The screenshot is the
|
|
184
|
+
* persona's only view of a canvas game, so the image stays and the flag
|
|
185
|
+
* goes: the text already says the action did not work.
|
|
186
|
+
* - **Content is never empty.** An empty string, an empty array or a text
|
|
187
|
+
* block with no text becomes {@link EMPTY_TOOL_RESULT_TEXT}; a non-string,
|
|
188
|
+
* non-array content becomes its string form.
|
|
189
|
+
* - **Exactly one result per tool_use, in the very next user message.** A
|
|
190
|
+
* missing result is added ({@link MISSING_TOOL_RESULT_TEXT}); a second
|
|
191
|
+
* result for one id, and a result whose id the assistant turn before it
|
|
192
|
+
* never used, are dropped. A user message left with nothing gets the empty
|
|
193
|
+
* text, because an empty message is refused too.
|
|
194
|
+
*
|
|
195
|
+
* A new array; the input is not mutated.
|
|
196
|
+
*
|
|
197
|
+
* @param {Array} messages
|
|
198
|
+
* @returns {Array}
|
|
199
|
+
*/
|
|
200
|
+
export function validToolResults(messages) {
|
|
201
|
+
const out = messages.map(message => message);
|
|
202
|
+
for (let i = 0; i < out.length; i += 1) {
|
|
203
|
+
const message = out[i];
|
|
204
|
+
if (!message || message.role !== 'user' || !Array.isArray(message.content)) continue;
|
|
205
|
+
const previous = i > 0 ? out[i - 1] : null;
|
|
206
|
+
const expected = previous && previous.role === 'assistant' && Array.isArray(previous.content)
|
|
207
|
+
? previous.content.filter(block => block && block.type === 'tool_use').map(block => block.id)
|
|
208
|
+
: [];
|
|
209
|
+
const seen = new Set();
|
|
210
|
+
const content = [];
|
|
211
|
+
for (const block of message.content) {
|
|
212
|
+
if (!block || block.type !== 'tool_result') {
|
|
213
|
+
content.push(block);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (!expected.includes(block.tool_use_id) || seen.has(block.tool_use_id)) continue;
|
|
217
|
+
seen.add(block.tool_use_id);
|
|
218
|
+
content.push(validToolResult(block));
|
|
219
|
+
}
|
|
220
|
+
const missing = expected.filter(id => !seen.has(id))
|
|
221
|
+
.map(id => ({ type: 'tool_result', tool_use_id: id, content: [{ type: 'text', text: MISSING_TOOL_RESULT_TEXT }] }));
|
|
222
|
+
// Results go first in the message: the API wants them before any text.
|
|
223
|
+
const results = [...content.filter(block => block && block.type === 'tool_result'), ...missing];
|
|
224
|
+
const rest = content.filter(block => !(block && block.type === 'tool_result'));
|
|
225
|
+
const next = [...results, ...rest];
|
|
226
|
+
out[i] = { ...message, content: next.length > 0 ? next : [{ type: 'text', text: EMPTY_TOOL_RESULT_TEXT }] };
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* One tool_result made valid. See {@link validToolResults}.
|
|
233
|
+
*
|
|
234
|
+
* @param {Object} block
|
|
235
|
+
* @returns {Object}
|
|
236
|
+
*/
|
|
237
|
+
export function validToolResult(block) {
|
|
238
|
+
let content = block.content;
|
|
239
|
+
if (typeof content === 'string') {
|
|
240
|
+
content = content.trim() ? content : EMPTY_TOOL_RESULT_TEXT;
|
|
241
|
+
} else if (Array.isArray(content)) {
|
|
242
|
+
content = content
|
|
243
|
+
.filter(child => child && (child.type !== 'text' || (typeof child.text === 'string' && child.text.trim())))
|
|
244
|
+
.map(child => child);
|
|
245
|
+
if (content.length === 0) content = [{ type: 'text', text: EMPTY_TOOL_RESULT_TEXT }];
|
|
246
|
+
} else {
|
|
247
|
+
content = content === undefined || content === null || String(content).trim() === '' ? EMPTY_TOOL_RESULT_TEXT : String(content);
|
|
248
|
+
}
|
|
249
|
+
const result = { ...block, content };
|
|
250
|
+
if (result.is_error && Array.isArray(content) && content.some(child => child.type !== 'text')) delete result.is_error;
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Every tool_result in the newest user message rewritten as plain text with
|
|
256
|
+
* no `is_error` flag and no image: the one repair the persona loop makes
|
|
257
|
+
* when the provider refuses a turn it cannot otherwise explain, before it
|
|
258
|
+
* gives up on the conversation. The text keeps what the persona was told.
|
|
259
|
+
*
|
|
260
|
+
* @param {Array} messages
|
|
261
|
+
* @returns {Array} a new array; the input is not mutated
|
|
262
|
+
*/
|
|
263
|
+
export function repairLastTurn(messages) {
|
|
264
|
+
const out = validToolResults(messages);
|
|
265
|
+
for (let i = out.length - 1; i >= 0; i -= 1) {
|
|
266
|
+
const message = out[i];
|
|
267
|
+
if (!message || message.role !== 'user' || !Array.isArray(message.content)) continue;
|
|
268
|
+
out[i] = {
|
|
269
|
+
...message,
|
|
270
|
+
content: message.content.map(block => {
|
|
271
|
+
if (!block || block.type !== 'tool_result') return block;
|
|
272
|
+
const texts = Array.isArray(block.content)
|
|
273
|
+
? block.content.filter(child => child.type === 'text').map(child => child.text)
|
|
274
|
+
: [String(block.content)];
|
|
275
|
+
const text = texts.join('\n').trim() || EMPTY_TOOL_RESULT_TEXT;
|
|
276
|
+
const { is_error: _flag, ...rest } = block;
|
|
277
|
+
return { ...rest, content: [{ type: 'text', text }] };
|
|
278
|
+
})
|
|
279
|
+
};
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
|
|
168
285
|
/**
|
|
169
286
|
* Both ceilings, in the order that matters: drop stale observations first
|
|
170
287
|
* (which removes their images too), then enforce the image budget on what
|
|
171
|
-
* is left
|
|
288
|
+
* is left, then make every tool_result one the API accepts (which has to
|
|
289
|
+
* come last: whether an `is_error` result still carries an image is only
|
|
290
|
+
* known once the image budget is applied).
|
|
172
291
|
*
|
|
173
292
|
* @param {Array} messages
|
|
174
293
|
* @param {{maxImages?: number, keepObservations?: number}} [opts]
|
|
175
294
|
* @returns {Array}
|
|
176
295
|
*/
|
|
177
296
|
export function prepareMessages(messages, { maxImages = MAX_IMAGES_IN_CONTEXT, keepObservations = KEEP_OBSERVATIONS } = {}) {
|
|
178
|
-
return pruneImages(pruneObservations(messages, keepObservations), maxImages);
|
|
297
|
+
return validToolResults(pruneImages(pruneObservations(messages, keepObservations), maxImages));
|
|
179
298
|
}
|
|
180
299
|
|
|
181
300
|
/**
|
|
@@ -189,4 +308,4 @@ export function shrinkImages(current) {
|
|
|
189
308
|
return Math.max(0, Math.floor(current / 2));
|
|
190
309
|
}
|
|
191
310
|
|
|
192
|
-
export default { prepareMessages, pruneImages, pruneObservations, countImages, shrinkImages, MAX_IMAGES_IN_CONTEXT, KEEP_OBSERVATIONS };
|
|
311
|
+
export default { prepareMessages, validToolResults, validToolResult, repairLastTurn, pruneImages, pruneObservations, countImages, shrinkImages, MAX_IMAGES_IN_CONTEXT, KEEP_OBSERVATIONS };
|