@frockbot/plugin-shell 0.3.15 → 0.3.17
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/frockbot.json +1 -1
- package/package.json +32 -32
- package/src/agent.test.ts +85 -2
- package/src/agent.ts +87 -20
- package/src/backend-applets.ts +2 -2
- package/src/backend-completion.test.ts +22 -5
- package/src/backend-composition.ts +20 -0
- package/src/backend-configuration.test.ts +7 -5
- package/src/backend-debug.test.ts +49 -0
- package/src/backend-flock.ts +119 -10
- package/src/backend-recovery-integration.test.ts +17 -3
- package/src/backend-runner.ts +1 -0
- package/src/backend.ts +133 -12
- package/src/client/FrockBotApp.vue +6 -1
- package/src/client/index.test.ts +61 -0
- package/src/client/index.ts +43 -10
- package/src/client/mobile-safe-area.test.ts +25 -0
- package/src/client/styles.css +28 -0
- package/src/compaction.test.ts +26 -0
- package/src/compaction.ts +46 -14
- package/src/debug-protocol.ts +4 -3
- package/src/history.ts +8 -2
- package/src/run-failure-copy.test.ts +11 -0
- package/src/run-failure-copy.ts +5 -1
- package/src/run-protocol.test.ts +73 -4
- package/src/run-protocol.ts +118 -12
- package/src/shared.ts +3 -0
package/src/client/index.ts
CHANGED
|
@@ -363,6 +363,13 @@ function assistantMessage(
|
|
|
363
363
|
run: ClientRun,
|
|
364
364
|
notification: ClientNotificationIntent | undefined,
|
|
365
365
|
): WebChatMessage {
|
|
366
|
+
const syncNotice = run.events.find(
|
|
367
|
+
(event) => event.type === "computer/sync" && event.message,
|
|
368
|
+
)?.message;
|
|
369
|
+
const notice = (primary?: string): string | undefined =>
|
|
370
|
+
[primary, syncNotice]
|
|
371
|
+
.filter((part): part is string => Boolean(part))
|
|
372
|
+
.join(" ") || undefined;
|
|
366
373
|
if (run.status === "running") {
|
|
367
374
|
// A streaming Turn carries only the text the model has produced. Until
|
|
368
375
|
// there is any, the thread shows the animated avatar and no bubble.
|
|
@@ -375,6 +382,7 @@ function assistantMessage(
|
|
|
375
382
|
// A Turn that has not started shows nothing of its own: the greyed user
|
|
376
383
|
// message is the whole of what the thread says about it.
|
|
377
384
|
...(run.queued ? { pending: true } : {}),
|
|
385
|
+
...(syncNotice ? { notice: syncNotice } : {}),
|
|
378
386
|
tools: toolsFrom(run.events),
|
|
379
387
|
sends: [],
|
|
380
388
|
tasks: tasksFrom(run.events),
|
|
@@ -391,6 +399,7 @@ function assistantMessage(
|
|
|
391
399
|
role: "assistant",
|
|
392
400
|
text: visibleAssistantText(run),
|
|
393
401
|
status: "aborted",
|
|
402
|
+
...(syncNotice ? { notice: syncNotice } : {}),
|
|
394
403
|
tools: toolsFrom(run.events),
|
|
395
404
|
sends: [],
|
|
396
405
|
tasks: tasksFrom(run.events),
|
|
@@ -409,7 +418,7 @@ function assistantMessage(
|
|
|
409
418
|
* exactly like the Bot speaking.
|
|
410
419
|
*/
|
|
411
420
|
text: visibleAssistantText(run),
|
|
412
|
-
notice: "This reply stopped partway. Try again to continue it.",
|
|
421
|
+
notice: notice("This reply stopped partway. Try again to continue it."),
|
|
413
422
|
status: "reconciliation-required",
|
|
414
423
|
tools: toolsFrom(run.events),
|
|
415
424
|
sends: [],
|
|
@@ -422,7 +431,7 @@ function assistantMessage(
|
|
|
422
431
|
runId: run.runId,
|
|
423
432
|
role: "assistant",
|
|
424
433
|
text: visibleAssistantText(run),
|
|
425
|
-
notice: "You stopped this.",
|
|
434
|
+
notice: notice("You stopped this."),
|
|
426
435
|
status: "aborted",
|
|
427
436
|
tools: toolsFrom(run.events),
|
|
428
437
|
sends: [],
|
|
@@ -447,7 +456,7 @@ function assistantMessage(
|
|
|
447
456
|
// wire, so this keeps whatever that chose — the model-deadline copy says
|
|
448
457
|
// something the outcome alone cannot — and falls back to the same line a
|
|
449
458
|
// reply-less failure gets.
|
|
450
|
-
notice: knownFailureCopyV1(run.failure),
|
|
459
|
+
notice: notice(knownFailureCopyV1(run.failure)),
|
|
451
460
|
status: "error",
|
|
452
461
|
tools: toolsFrom(run.events),
|
|
453
462
|
sends: [],
|
|
@@ -465,8 +474,10 @@ function assistantMessage(
|
|
|
465
474
|
// Why the Turn ends there, under whatever it had already said — never as
|
|
466
475
|
// the bubble's own text, which reads as the Bot saying it.
|
|
467
476
|
...(run.status === "failed"
|
|
468
|
-
? { notice: knownFailureCopyV1(run.failure) }
|
|
469
|
-
:
|
|
477
|
+
? { notice: notice(knownFailureCopyV1(run.failure)) }
|
|
478
|
+
: syncNotice
|
|
479
|
+
? { notice: syncNotice }
|
|
480
|
+
: {}),
|
|
470
481
|
status: run.status === "failed" ? "error" : "completed",
|
|
471
482
|
tools: toolsFrom(run.events),
|
|
472
483
|
sends: [],
|
|
@@ -561,6 +572,7 @@ export function projectDurableRuns(
|
|
|
561
572
|
? { at: existingUser.at }
|
|
562
573
|
: {}),
|
|
563
574
|
status: "completed",
|
|
575
|
+
...(run.via ? { via: run.via } : {}),
|
|
564
576
|
// Greyed while its Turn waits, ordinary the moment it is running. The
|
|
565
577
|
// flag comes from durable run state, so a reload draws the same thing.
|
|
566
578
|
...(run.queued ? { pending: true } : {}),
|
|
@@ -3070,6 +3082,14 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
3070
3082
|
* and the one-bubble-per-send contract is untouched.
|
|
3071
3083
|
*/
|
|
3072
3084
|
let stopRunChannel: (() => void) | undefined;
|
|
3085
|
+
// The channel's own health, for the observation below: `fallback` means the
|
|
3086
|
+
// socket is gone and the transcript is flying blind, and each return to
|
|
3087
|
+
// `open` means it was gone for a while. Either is a reason to read the
|
|
3088
|
+
// running Turn from authority rather than trust a POST that may have died
|
|
3089
|
+
// with the same connection — which is how a phone kept drawing the working
|
|
3090
|
+
// trail for a Turn the server had already settled (2026-09-04).
|
|
3091
|
+
const channelFallback = ref(false);
|
|
3092
|
+
const channelReconnects = ref(0);
|
|
3073
3093
|
const stopRunChannelWatch = watch(
|
|
3074
3094
|
() => web.value.activeBotId,
|
|
3075
3095
|
(botId) => {
|
|
@@ -3096,10 +3116,14 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
3096
3116
|
restoredWithoutRead = undefined;
|
|
3097
3117
|
await deliverNotifications(botId, generation);
|
|
3098
3118
|
},
|
|
3099
|
-
status() {
|
|
3119
|
+
status(status) {
|
|
3100
3120
|
// The channel's health is not the transcript's: an unavailable
|
|
3101
3121
|
// socket falls back to the observation below, which is what a
|
|
3102
3122
|
// client without one uses anyway.
|
|
3123
|
+
if (generation !== selectionGeneration) return;
|
|
3124
|
+
const wasDown = channelFallback.value;
|
|
3125
|
+
channelFallback.value = status === "fallback";
|
|
3126
|
+
if (status === "open" && wasDown) channelReconnects.value += 1;
|
|
3103
3127
|
},
|
|
3104
3128
|
});
|
|
3105
3129
|
},
|
|
@@ -3107,12 +3131,21 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
3107
3131
|
);
|
|
3108
3132
|
|
|
3109
3133
|
const stopRunObservation = watch(
|
|
3110
|
-
() =>
|
|
3111
|
-
|
|
3134
|
+
() =>
|
|
3135
|
+
[
|
|
3136
|
+
web.value.activeBotId,
|
|
3137
|
+
web.value.activeRunId,
|
|
3138
|
+
channelFallback.value,
|
|
3139
|
+
channelReconnects.value,
|
|
3140
|
+
] as const,
|
|
3141
|
+
([botId, runId, fallback]) => {
|
|
3112
3142
|
// The send path owns the run it started: its POST is the observation,
|
|
3113
3143
|
// and `stopRun` starts its own. This is for every other way a client
|
|
3114
|
-
// finds itself watching a Turn it is not holding open
|
|
3115
|
-
|
|
3144
|
+
// finds itself watching a Turn it is not holding open — and, once the
|
|
3145
|
+
// state channel has dropped or come back, for the Turn it *is* holding
|
|
3146
|
+
// open, because that POST shared the connection that just failed.
|
|
3147
|
+
if (!botId || !runId || runObserver) return;
|
|
3148
|
+
if (activeRequest && !fallback && channelReconnects.value === 0) return;
|
|
3116
3149
|
const generation = selectionGeneration;
|
|
3117
3150
|
const observer = new AbortController();
|
|
3118
3151
|
runObserver = observer;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
const styles = await Bun.file(new URL("./styles.css", import.meta.url)).text();
|
|
4
|
+
|
|
5
|
+
describe("phone safe-area layout", () => {
|
|
6
|
+
test("keeps the conversation header and thread below the status bar", () => {
|
|
7
|
+
expect(styles).toContain(
|
|
8
|
+
"height: calc(var(--frock-titlebar-height) + var(--frock-safe-top));",
|
|
9
|
+
);
|
|
10
|
+
expect(styles).toContain("padding-top: calc(var(--frock-safe-top) + 0px);");
|
|
11
|
+
expect(styles).toContain(
|
|
12
|
+
"top: calc(var(--frock-titlebar-height) + var(--frock-safe-top));",
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("keeps drawers, Settings, and the composer clear of both bars", () => {
|
|
17
|
+
expect(styles).toContain(
|
|
18
|
+
"padding-bottom: calc(10px + var(--frock-safe-bottom));",
|
|
19
|
+
);
|
|
20
|
+
expect(styles).toContain(
|
|
21
|
+
"padding-bottom: calc(16px + var(--frock-safe-bottom));",
|
|
22
|
+
);
|
|
23
|
+
expect(styles).toContain("bottom: calc(10px + var(--frock-safe-bottom));");
|
|
24
|
+
});
|
|
25
|
+
});
|
package/src/client/styles.css
CHANGED
|
@@ -499,6 +499,17 @@
|
|
|
499
499
|
background: var(--frock-action-primary);
|
|
500
500
|
}
|
|
501
501
|
|
|
502
|
+
.message-user-column {
|
|
503
|
+
display: grid;
|
|
504
|
+
justify-items: end;
|
|
505
|
+
gap: 4px;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.message-via {
|
|
509
|
+
color: var(--frock-text-muted);
|
|
510
|
+
font-size: var(--frock-text-xs);
|
|
511
|
+
}
|
|
512
|
+
|
|
502
513
|
/*
|
|
503
514
|
* User-facing sends. They stack under the Bot's avatar in the same column the
|
|
504
515
|
* text bubble occupies, so a Turn that only sent a widget still reads as the
|
|
@@ -1118,6 +1129,7 @@
|
|
|
1118
1129
|
/* `calc` so the declaration is a length the checker can type; `env` alone
|
|
1119
1130
|
is not one it knows. */
|
|
1120
1131
|
padding-top: calc(var(--frock-safe-top) + 0px);
|
|
1132
|
+
padding-bottom: calc(10px + var(--frock-safe-bottom));
|
|
1121
1133
|
box-shadow: var(--frock-shadow-panel);
|
|
1122
1134
|
transform: translateX(-100%);
|
|
1123
1135
|
visibility: hidden;
|
|
@@ -1189,12 +1201,27 @@
|
|
|
1189
1201
|
/* No window chrome to clear, but the panel toggle still sits at the
|
|
1190
1202
|
trailing edge, so the row ends before it rather than under it. */
|
|
1191
1203
|
.topbar {
|
|
1204
|
+
height: calc(var(--frock-titlebar-height) + var(--frock-safe-top));
|
|
1192
1205
|
gap: 8px;
|
|
1193
1206
|
padding: 0 52px 0 4px;
|
|
1207
|
+
padding-top: calc(var(--frock-safe-top) + 0px);
|
|
1194
1208
|
}
|
|
1195
1209
|
|
|
1196
1210
|
.window-actions {
|
|
1211
|
+
height: calc(var(--frock-titlebar-height) + var(--frock-safe-top));
|
|
1197
1212
|
padding: 0 8px;
|
|
1213
|
+
padding-top: calc(var(--frock-safe-top) + 0px);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
.right-panel-header,
|
|
1217
|
+
.panel-surface-header {
|
|
1218
|
+
height: calc(var(--frock-titlebar-height) + var(--frock-safe-top));
|
|
1219
|
+
padding-top: calc(var(--frock-safe-top) + 0px);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
.right-panel-body,
|
|
1223
|
+
.panel-surface-content {
|
|
1224
|
+
padding-bottom: calc(16px + var(--frock-safe-bottom));
|
|
1198
1225
|
}
|
|
1199
1226
|
|
|
1200
1227
|
.brand-mark {
|
|
@@ -1211,6 +1238,7 @@
|
|
|
1211
1238
|
* vertical anchors all clear the home indicator.
|
|
1212
1239
|
*/
|
|
1213
1240
|
.thread {
|
|
1241
|
+
top: calc(var(--frock-titlebar-height) + var(--frock-safe-top));
|
|
1214
1242
|
bottom: calc(76px + var(--frock-safe-bottom));
|
|
1215
1243
|
padding: 12px 12px 20px;
|
|
1216
1244
|
}
|
package/src/compaction.test.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
parseCompactionSummaryV1,
|
|
16
16
|
PRUNED_TOOL_RESULT_V1,
|
|
17
17
|
pruneToolOutputsV1,
|
|
18
|
+
renderCompactionSummaryV1,
|
|
18
19
|
runCompactionV1,
|
|
19
20
|
COMPACTION_TRIGGER_RATIO_V1,
|
|
20
21
|
} from "./compaction.js";
|
|
@@ -385,6 +386,31 @@ describe("injecting a compaction", () => {
|
|
|
385
386
|
});
|
|
386
387
|
|
|
387
388
|
describe("reading the summariser's answer", () => {
|
|
389
|
+
test("renders the validated fields into the durable heading format", () => {
|
|
390
|
+
expect(
|
|
391
|
+
renderCompactionSummaryV1({
|
|
392
|
+
summary: "Work on the Applet.",
|
|
393
|
+
decisions: ["Keep the first design."],
|
|
394
|
+
openItems: [],
|
|
395
|
+
identifiers: ["applet-9f2c"],
|
|
396
|
+
}),
|
|
397
|
+
).toBe(
|
|
398
|
+
[
|
|
399
|
+
"## Summary",
|
|
400
|
+
"Work on the Applet.",
|
|
401
|
+
"",
|
|
402
|
+
"## Decisions",
|
|
403
|
+
"- Keep the first design.",
|
|
404
|
+
"",
|
|
405
|
+
"## Open items",
|
|
406
|
+
"- none",
|
|
407
|
+
"",
|
|
408
|
+
"## Identifiers mentioned",
|
|
409
|
+
"- applet-9f2c",
|
|
410
|
+
].join("\n"),
|
|
411
|
+
);
|
|
412
|
+
});
|
|
413
|
+
|
|
388
414
|
test("lifts the identifiers out of their heading", () => {
|
|
389
415
|
const parsed = parseCompactionSummaryV1(
|
|
390
416
|
[
|
package/src/compaction.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type ModelBindingSnapshot,
|
|
27
27
|
type Session,
|
|
28
28
|
type SessionEvent,
|
|
29
|
+
type StructuredOutputSchemaV1,
|
|
29
30
|
} from "@frockbot/kernel-contracts";
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -262,27 +263,58 @@ export function assessCompactionV1(input: {
|
|
|
262
263
|
* ids in passing, and the list is what the event stores.
|
|
263
264
|
*/
|
|
264
265
|
export const COMPACTION_SYSTEM_PROMPT_V1 = [
|
|
265
|
-
"You are compressing the earlier part of a conversation so it can be carried forward in a smaller prompt.
|
|
266
|
+
"You are compressing the earlier part of a conversation so it can be carried forward in a smaller prompt.",
|
|
266
267
|
"",
|
|
267
268
|
"CRITICAL: You MUST preserve ALL opaque identifiers exactly as they appear. That includes UUIDs, hashes, full URLs with their query parameters, file and Workspace paths, Package ids, Applet ids, Bot ids, Session ids, tool call ids, model names and version strings. Do NOT paraphrase, abbreviate, or generalise an identifier. Copy it exactly.",
|
|
268
269
|
"",
|
|
269
|
-
"
|
|
270
|
-
"",
|
|
271
|
-
"## Summary",
|
|
272
|
-
"What the conversation is about and what has happened, in a few short paragraphs or bullets.",
|
|
273
|
-
"",
|
|
274
|
-
"## Decisions",
|
|
275
|
-
"Decisions made and the reason for each. Where a decision was later changed, keep only the latest and say it superseded an earlier one.",
|
|
276
|
-
"",
|
|
277
|
-
"## Open items",
|
|
278
|
-
"Work that is pending, promised, or unfinished. Be specific about what is owed and by whom.",
|
|
279
|
-
"",
|
|
280
|
-
"## Identifiers mentioned",
|
|
281
|
-
"A bullet list of every opaque identifier that appeared, one per line, copied exactly. Write `- none` if there were none.",
|
|
270
|
+
"Put the gist in `summary`, decisions and their reasons in `decisions`, pending work in `openItems`, and every opaque identifier copied exactly in `identifiers`. Keep only the latest decision where one superseded another.",
|
|
282
271
|
"",
|
|
283
272
|
"Leave out pleasantries, repetition, and superseded detail. Do not invent anything that is not in the transcript. Do not address the user.",
|
|
284
273
|
].join("\n");
|
|
285
274
|
|
|
275
|
+
export interface CompactionSummaryPayloadV1 {
|
|
276
|
+
summary: string;
|
|
277
|
+
decisions: string[];
|
|
278
|
+
openItems: string[];
|
|
279
|
+
identifiers: string[];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** The actual production consumer of the shared structured-output seam. */
|
|
283
|
+
export const COMPACTION_RESPONSE_SCHEMA_V1 = {
|
|
284
|
+
type: "object",
|
|
285
|
+
properties: {
|
|
286
|
+
summary: { type: "string" },
|
|
287
|
+
decisions: { type: "array", items: { type: "string" } },
|
|
288
|
+
openItems: { type: "array", items: { type: "string" } },
|
|
289
|
+
identifiers: { type: "array", items: { type: "string" } },
|
|
290
|
+
},
|
|
291
|
+
required: ["summary", "decisions", "openItems", "identifiers"],
|
|
292
|
+
additionalProperties: false,
|
|
293
|
+
} as const satisfies StructuredOutputSchemaV1;
|
|
294
|
+
|
|
295
|
+
/** Keeps the durable summary format readable while model I/O stays typed. */
|
|
296
|
+
export function renderCompactionSummaryV1(
|
|
297
|
+
payload: CompactionSummaryPayloadV1,
|
|
298
|
+
): string {
|
|
299
|
+
const bullets = (values: readonly string[]) =>
|
|
300
|
+
values.length > 0
|
|
301
|
+
? values.map((value) => `- ${value}`).join("\n")
|
|
302
|
+
: "- none";
|
|
303
|
+
return [
|
|
304
|
+
"## Summary",
|
|
305
|
+
payload.summary,
|
|
306
|
+
"",
|
|
307
|
+
"## Decisions",
|
|
308
|
+
bullets(payload.decisions),
|
|
309
|
+
"",
|
|
310
|
+
"## Open items",
|
|
311
|
+
bullets(payload.openItems),
|
|
312
|
+
"",
|
|
313
|
+
"## Identifiers mentioned",
|
|
314
|
+
bullets(payload.identifiers),
|
|
315
|
+
].join("\n");
|
|
316
|
+
}
|
|
317
|
+
|
|
286
318
|
/** The transcript one summariser call is given, flattened to plain text. */
|
|
287
319
|
export function compactionTranscriptV1(
|
|
288
320
|
messages: readonly LlmMessage[],
|
package/src/debug-protocol.ts
CHANGED
|
@@ -20,9 +20,10 @@ export const BOT_DEBUG_RUN_LIMIT_V1 = 20;
|
|
|
20
20
|
export const BOT_DEBUG_DEFAULT_RUN_LIMIT_V1 = 5;
|
|
21
21
|
export const BOT_DEBUG_GENERATION_LIMIT_V1 = 5;
|
|
22
22
|
/**
|
|
23
|
-
* The event budget one snapshot spends.
|
|
24
|
-
* a handful of runs can
|
|
25
|
-
*
|
|
23
|
+
* The event budget one snapshot spends. Large durable events are already
|
|
24
|
+
* bounded diagnostic projections, but a handful of runs can still accumulate
|
|
25
|
+
* many of them; past this the *oldest* events of a run are dropped, because a
|
|
26
|
+
* failure is described by the tail of its log.
|
|
26
27
|
*/
|
|
27
28
|
export const BOT_DEBUG_EVENT_BYTES_V1 = 512_000;
|
|
28
29
|
|
package/src/history.ts
CHANGED
|
@@ -175,7 +175,10 @@ export function chatWindowV1(
|
|
|
175
175
|
): ChatWindowV1 {
|
|
176
176
|
const turns = messageTurnsV1(events);
|
|
177
177
|
const types = turnTypesByTurnV1(events);
|
|
178
|
-
const chat = (turn: number) =>
|
|
178
|
+
const chat = (turn: number) => {
|
|
179
|
+
const type = types.get(turn) ?? "chat";
|
|
180
|
+
return type === "chat" || type === "agent";
|
|
181
|
+
};
|
|
179
182
|
const state = compactionStateV1(events);
|
|
180
183
|
const current = currentTurnV1(events);
|
|
181
184
|
// A compaction never covers the Turn being assembled, whatever the log says:
|
|
@@ -229,7 +232,10 @@ export function turnScopedMessagesV1(
|
|
|
229
232
|
}
|
|
230
233
|
const types = turnTypesByTurnV1(input.events);
|
|
231
234
|
const current = currentTurnV1(input.events);
|
|
232
|
-
const chatTurn = (turn: number) =>
|
|
235
|
+
const chatTurn = (turn: number) => {
|
|
236
|
+
const type = types.get(turn) ?? "chat";
|
|
237
|
+
return type === "chat" || type === "agent";
|
|
238
|
+
};
|
|
233
239
|
if (chatTurn(current)) {
|
|
234
240
|
const window = chatWindowV1(input.events, input.messages);
|
|
235
241
|
// Tier 1 of ADR 0030, and the only one that costs nothing: a tool result
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { STEP_LIMIT_REASON_V1 } from "@frockbot/kernel-agent-loop";
|
|
1
2
|
import { describe, expect, test } from "bun:test";
|
|
2
3
|
import type { SessionEvent } from "@frockbot/kernel-contracts";
|
|
3
4
|
import { MODEL_FIRST_BYTE_DEADLINE_REASON_V1 } from "@frockbot/kernel-contracts";
|
|
@@ -157,3 +158,13 @@ describe("runFailureCopyV1", () => {
|
|
|
157
158
|
expect(projected.outcome.message).toBe(RUN_FAILURE_COPY_V1.interrupted);
|
|
158
159
|
});
|
|
159
160
|
});
|
|
161
|
+
|
|
162
|
+
test("a reply that ran out of steps says so in plain words", () => {
|
|
163
|
+
// `kernel-do` wraps the loop's reason; the sentence survives the wrapper.
|
|
164
|
+
expect(
|
|
165
|
+
runFailureCopyV1({
|
|
166
|
+
failure: `Bot turn ended with outcome interrupted: ${STEP_LIMIT_REASON_V1}`,
|
|
167
|
+
}),
|
|
168
|
+
).toBe(STEP_LIMIT_REASON_V1);
|
|
169
|
+
expect(knownFailureCopyV1(STEP_LIMIT_REASON_V1)).toBe(STEP_LIMIT_REASON_V1);
|
|
170
|
+
});
|
package/src/run-failure-copy.ts
CHANGED
|
@@ -7,7 +7,10 @@ import {
|
|
|
7
7
|
MODEL_FIRST_BYTE_DEADLINE_REASON_V1,
|
|
8
8
|
MODEL_IDLE_DEADLINE_REASON_V1,
|
|
9
9
|
} from "@frockbot/kernel-contracts";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
STEP_LIMIT_REASON_V1,
|
|
12
|
+
TURN_DEADLINE_REASON_V1,
|
|
13
|
+
} from "@frockbot/kernel-agent-loop";
|
|
11
14
|
import { UNRECONCILABLE_RUN_FAILURE_V1 } from "@frockbot/kernel-do";
|
|
12
15
|
|
|
13
16
|
/**
|
|
@@ -47,6 +50,7 @@ export const USER_FACING_FAILURE_REASONS_V1: readonly string[] = [
|
|
|
47
50
|
MODEL_FIRST_BYTE_DEADLINE_REASON_V1,
|
|
48
51
|
MODEL_IDLE_DEADLINE_REASON_V1,
|
|
49
52
|
TURN_DEADLINE_REASON_V1,
|
|
53
|
+
STEP_LIMIT_REASON_V1,
|
|
50
54
|
UNRECONCILABLE_RUN_FAILURE_V1,
|
|
51
55
|
];
|
|
52
56
|
|
package/src/run-protocol.test.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
projectClientRunLookupV1,
|
|
25
25
|
projectClientRunListV1,
|
|
26
26
|
projectClientRunV1,
|
|
27
|
+
isVisibleRunV1,
|
|
27
28
|
projectClientRunOrDegradedV1,
|
|
28
29
|
projectClientTurnV1,
|
|
29
30
|
UNRECORDED_TOOL_RESULT_TEXT_V1,
|
|
@@ -88,6 +89,37 @@ function storedRun(
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
describe("client run protocol v1", () => {
|
|
92
|
+
test("projects an agent Turn with its Bot origin marker", () => {
|
|
93
|
+
const agent = {
|
|
94
|
+
...storedRun([]),
|
|
95
|
+
admission: {
|
|
96
|
+
schemaVersion: 1 as const,
|
|
97
|
+
turnType: "agent" as const,
|
|
98
|
+
origin: {
|
|
99
|
+
kind: "bot" as const,
|
|
100
|
+
fromBotId: "researcher",
|
|
101
|
+
fromBotName: "Researcher",
|
|
102
|
+
messageId: "message-1",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
expect(isVisibleRunV1(agent)).toBe(true);
|
|
108
|
+
const projected = projectClientRunV1(agent);
|
|
109
|
+
expect(projected).toMatchObject({
|
|
110
|
+
schemaVersion: 3,
|
|
111
|
+
input: "continue",
|
|
112
|
+
via: { kind: "bot", name: "Researcher", botId: "researcher" },
|
|
113
|
+
});
|
|
114
|
+
expect(
|
|
115
|
+
decodeClientRunListV1({
|
|
116
|
+
schemaVersion: 1,
|
|
117
|
+
runs: [projected],
|
|
118
|
+
page: { truncated: false },
|
|
119
|
+
})[0]?.via,
|
|
120
|
+
).toEqual(projected.via);
|
|
121
|
+
});
|
|
122
|
+
|
|
91
123
|
test("rejects durable runs missing current admission fields", () => {
|
|
92
124
|
const complete = storedRun([], "running");
|
|
93
125
|
for (const field of [
|
|
@@ -728,7 +760,7 @@ describe("client run protocol v1", () => {
|
|
|
728
760
|
schemaVersion: 1,
|
|
729
761
|
runs: [
|
|
730
762
|
{
|
|
731
|
-
schemaVersion:
|
|
763
|
+
schemaVersion: 3,
|
|
732
764
|
runId: "run-1",
|
|
733
765
|
admittedAt: timestamp,
|
|
734
766
|
input: "continue",
|
|
@@ -977,9 +1009,8 @@ describe("client run protocol v1", () => {
|
|
|
977
1009
|
]),
|
|
978
1010
|
);
|
|
979
1011
|
|
|
980
|
-
// Version
|
|
981
|
-
|
|
982
|
-
expect(projected.schemaVersion).toBe(2);
|
|
1012
|
+
// Version 3 carries agent-origin markers; older bodies still decode.
|
|
1013
|
+
expect(projected.schemaVersion).toBe(3);
|
|
983
1014
|
expect(projected.events).toEqual([
|
|
984
1015
|
{ type: "send/to-user", payload: { type: "text", text: "On it." } },
|
|
985
1016
|
{ type: "tool/call", call: { id: "tool-1", name: "lookup" } },
|
|
@@ -1614,3 +1645,41 @@ describe("dispatched subagents in the run projection", () => {
|
|
|
1614
1645
|
);
|
|
1615
1646
|
});
|
|
1616
1647
|
});
|
|
1648
|
+
|
|
1649
|
+
describe("Computer sync degradation in the run projection", () => {
|
|
1650
|
+
const degraded: SessionEvent = {
|
|
1651
|
+
type: "computer/sync",
|
|
1652
|
+
seq: 4,
|
|
1653
|
+
timestamp,
|
|
1654
|
+
turn: 1,
|
|
1655
|
+
reason: "open",
|
|
1656
|
+
status: "degraded",
|
|
1657
|
+
detail: "Excluded 2 reproducible Workspace items from sync.",
|
|
1658
|
+
pulled: 0,
|
|
1659
|
+
pushed: 1,
|
|
1660
|
+
restored: 0,
|
|
1661
|
+
removed: 0,
|
|
1662
|
+
adopted: 0,
|
|
1663
|
+
conflicts: 0,
|
|
1664
|
+
failures: 0,
|
|
1665
|
+
ignored: 2,
|
|
1666
|
+
omitted: 0,
|
|
1667
|
+
};
|
|
1668
|
+
|
|
1669
|
+
test("projects one plain-language event and round-trips it", () => {
|
|
1670
|
+
const projected = projectClientRunV1(
|
|
1671
|
+
storedRun([degraded, { ...degraded, seq: 5, reason: "turn-end" }]),
|
|
1672
|
+
);
|
|
1673
|
+
expect(projected.events).toEqual([
|
|
1674
|
+
{
|
|
1675
|
+
type: "computer/sync",
|
|
1676
|
+
status: "degraded",
|
|
1677
|
+
message: "Excluded 2 reproducible Workspace items from sync.",
|
|
1678
|
+
},
|
|
1679
|
+
]);
|
|
1680
|
+
const page = createClientRunListV1([projected], { truncated: false });
|
|
1681
|
+
expect(
|
|
1682
|
+
decodeClientRunPageV1(structuredClone(page)).runs[0]?.events,
|
|
1683
|
+
).toEqual(projected.events);
|
|
1684
|
+
});
|
|
1685
|
+
});
|