@frockbot/plugin-shell 0.3.16 → 0.3.18
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 +33 -32
- package/src/agent.test.ts +85 -2
- package/src/agent.ts +87 -20
- package/src/backend-applets.ts +2 -2
- package/src/backend-composition.ts +20 -0
- package/src/backend-flock.ts +119 -10
- package/src/backend-recovery-integration.test.ts +1 -1
- package/src/backend-runner.ts +1 -0
- package/src/backend.ts +100 -1
- 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/client/voice-microphone.ts +13 -3
- package/src/compaction.test.ts +26 -0
- package/src/compaction.ts +46 -14
- 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/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
|
+
});
|
package/src/run-protocol.ts
CHANGED
|
@@ -165,6 +165,11 @@ export type ClientRunEventV1 =
|
|
|
165
165
|
type: "wake/parent";
|
|
166
166
|
message: string;
|
|
167
167
|
}
|
|
168
|
+
| {
|
|
169
|
+
type: "computer/sync";
|
|
170
|
+
status: "degraded" | "unavailable" | "refused" | "skipped";
|
|
171
|
+
message: string;
|
|
172
|
+
}
|
|
168
173
|
/**
|
|
169
174
|
* A subagent this Turn dispatched (ADR 0017). The child's Session never
|
|
170
175
|
* enters the visible transcript, so this chip is the whole of what the
|
|
@@ -212,12 +217,12 @@ export interface ClientRunRecoveryV1 {
|
|
|
212
217
|
}
|
|
213
218
|
|
|
214
219
|
/**
|
|
215
|
-
* The run projection. Version 2
|
|
216
|
-
* `events
|
|
217
|
-
*
|
|
220
|
+
* The run projection. Version 2 added structured `send/to-user` and
|
|
221
|
+
* `wake/parent` events; version 3 adds the bounded `via` marker for agent and
|
|
222
|
+
* Voice Turns without exposing the internal origin record.
|
|
218
223
|
*/
|
|
219
224
|
export interface ClientRunV1 {
|
|
220
|
-
schemaVersion: 1 | 2;
|
|
225
|
+
schemaVersion: 1 | 2 | 3;
|
|
221
226
|
runId: string;
|
|
222
227
|
admittedAt: string;
|
|
223
228
|
input: string;
|
|
@@ -241,6 +246,10 @@ export interface ClientRunV1 {
|
|
|
241
246
|
partialText?: string;
|
|
242
247
|
outcome?: ClientRunOutcomeV1;
|
|
243
248
|
recovery?: ClientRunRecoveryV1;
|
|
249
|
+
/** Where an agent-lane question entered this Bot's transcript. */
|
|
250
|
+
via?:
|
|
251
|
+
| { kind: "bot"; name: string; botId: string }
|
|
252
|
+
| { kind: "voice"; name: "Voice" };
|
|
244
253
|
}
|
|
245
254
|
|
|
246
255
|
export interface ClientRunPageV1 {
|
|
@@ -624,6 +633,7 @@ function projectionUnits(
|
|
|
624
633
|
const units: ProjectionUnitV1[] = [];
|
|
625
634
|
const byOccurrence = new Map<string, ProjectionUnitV1>();
|
|
626
635
|
let callCount = 0;
|
|
636
|
+
let projectedIncompleteSync = false;
|
|
627
637
|
for (const event of events) {
|
|
628
638
|
if (event.type === "tool/call") {
|
|
629
639
|
if (byOccurrence.has(event.occurrenceId)) {
|
|
@@ -694,6 +704,22 @@ function projectionUnits(
|
|
|
694
704
|
],
|
|
695
705
|
droppable: true,
|
|
696
706
|
});
|
|
707
|
+
} else if (
|
|
708
|
+
event.type === "computer/sync" &&
|
|
709
|
+
event.status !== "ok" &&
|
|
710
|
+
!projectedIncompleteSync
|
|
711
|
+
) {
|
|
712
|
+
projectedIncompleteSync = true;
|
|
713
|
+
units.push({
|
|
714
|
+
events: [
|
|
715
|
+
{
|
|
716
|
+
type: "computer/sync",
|
|
717
|
+
status: event.status,
|
|
718
|
+
message: computerSyncCopyV1(event),
|
|
719
|
+
},
|
|
720
|
+
],
|
|
721
|
+
droppable: true,
|
|
722
|
+
});
|
|
697
723
|
} else if (event.type === "task/dispatched") {
|
|
698
724
|
units.push({
|
|
699
725
|
events: [
|
|
@@ -736,6 +762,24 @@ function projectionUnits(
|
|
|
736
762
|
return units;
|
|
737
763
|
}
|
|
738
764
|
|
|
765
|
+
function computerSyncCopyV1(
|
|
766
|
+
event: Extract<SessionEvent, { type: "computer/sync" }>,
|
|
767
|
+
): string {
|
|
768
|
+
if (event.status === "degraded") {
|
|
769
|
+
return (
|
|
770
|
+
event.detail.trim() ||
|
|
771
|
+
"Some Workspace files did not sync during this turn."
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
if (event.status === "unavailable") {
|
|
775
|
+
return "Workspace files could not sync during this turn. Sync will retry the next time the Computer is used.";
|
|
776
|
+
}
|
|
777
|
+
if (event.status === "refused") {
|
|
778
|
+
return "Workspace sync could not run for this turn.";
|
|
779
|
+
}
|
|
780
|
+
return "Workspace sync was skipped for this turn.";
|
|
781
|
+
}
|
|
782
|
+
|
|
739
783
|
function visibleEvents(
|
|
740
784
|
events: readonly SessionEvent[],
|
|
741
785
|
status: ClientRunStatusV1,
|
|
@@ -876,10 +920,20 @@ export function projectClientRunV1(run: StoredRun): ClientRunV1 {
|
|
|
876
920
|
message: RESUMABLE_RUN_MESSAGE_V1,
|
|
877
921
|
} satisfies ClientRunRecoveryV1)
|
|
878
922
|
: undefined;
|
|
923
|
+
const origin = run.admission?.origin;
|
|
924
|
+
const via =
|
|
925
|
+
origin?.kind === "bot"
|
|
926
|
+
? {
|
|
927
|
+
kind: "bot" as const,
|
|
928
|
+
name: truncateWireString(origin.fromBotName, 100),
|
|
929
|
+
botId: truncate(origin.fromBotId, 128),
|
|
930
|
+
}
|
|
931
|
+
: origin?.kind === "voice"
|
|
932
|
+
? { kind: "voice" as const, name: "Voice" as const }
|
|
933
|
+
: undefined;
|
|
879
934
|
return {
|
|
880
|
-
// Version
|
|
881
|
-
|
|
882
|
-
schemaVersion: 2,
|
|
935
|
+
// Version 3 adds the origin marker for an agent-lane message.
|
|
936
|
+
schemaVersion: 3,
|
|
883
937
|
runId: truncate(run.runId, MAX_RUN_ID_LENGTH),
|
|
884
938
|
admittedAt: truncate(run.acceptedAt, MAX_TIMESTAMP_LENGTH),
|
|
885
939
|
input: truncateWireString(run.input, MAX_INPUT_BYTES),
|
|
@@ -896,6 +950,7 @@ export function projectClientRunV1(run: StoredRun): ClientRunV1 {
|
|
|
896
950
|
: {}),
|
|
897
951
|
...(outcome ? { outcome } : {}),
|
|
898
952
|
...(recovery ? { recovery } : {}),
|
|
953
|
+
...(via ? { via } : {}),
|
|
899
954
|
};
|
|
900
955
|
}
|
|
901
956
|
|
|
@@ -921,7 +976,8 @@ function lookupState(
|
|
|
921
976
|
export function isVisibleRunV1(run: {
|
|
922
977
|
admission?: { turnType?: string };
|
|
923
978
|
}): boolean {
|
|
924
|
-
|
|
979
|
+
const type = run.admission?.turnType ?? "chat";
|
|
980
|
+
return type === "chat" || type === "agent";
|
|
925
981
|
}
|
|
926
982
|
|
|
927
983
|
export function projectClientRunLookupV1(
|
|
@@ -1248,6 +1304,27 @@ function decodeEvent(value: unknown): ClientRunEventV1 {
|
|
|
1248
1304
|
),
|
|
1249
1305
|
};
|
|
1250
1306
|
}
|
|
1307
|
+
if (event.type === "computer/sync") {
|
|
1308
|
+
exactKeys(event, ["type", "status", "message"], "run event");
|
|
1309
|
+
if (
|
|
1310
|
+
event.status !== "degraded" &&
|
|
1311
|
+
event.status !== "unavailable" &&
|
|
1312
|
+
event.status !== "refused" &&
|
|
1313
|
+
event.status !== "skipped"
|
|
1314
|
+
) {
|
|
1315
|
+
throw new Error("run event.status is invalid");
|
|
1316
|
+
}
|
|
1317
|
+
return {
|
|
1318
|
+
type: "computer/sync",
|
|
1319
|
+
status: event.status,
|
|
1320
|
+
message: wireString(
|
|
1321
|
+
event,
|
|
1322
|
+
"message",
|
|
1323
|
+
MAX_EVENT_CONTENT_BYTES,
|
|
1324
|
+
"run event",
|
|
1325
|
+
),
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1251
1328
|
if (event.type === "task/dispatched") {
|
|
1252
1329
|
exactKeys(
|
|
1253
1330
|
event,
|
|
@@ -1299,12 +1376,13 @@ function decodeEvents(values: unknown[]): ClientTurnEvent[] {
|
|
|
1299
1376
|
const callIds = new Set<string>();
|
|
1300
1377
|
while (index < events.length) {
|
|
1301
1378
|
const call = events[index];
|
|
1302
|
-
//
|
|
1303
|
-
// call/result walk steps straight over them.
|
|
1379
|
+
// Sends, hand-offs, task chips, and sync notices stand alone: they pair
|
|
1380
|
+
// with nothing, so the call/result walk steps straight over them.
|
|
1304
1381
|
if (
|
|
1305
1382
|
call?.type === "send/to-user" ||
|
|
1306
1383
|
call?.type === "wake/parent" ||
|
|
1307
|
-
call?.type === "task/dispatched"
|
|
1384
|
+
call?.type === "task/dispatched" ||
|
|
1385
|
+
call?.type === "computer/sync"
|
|
1308
1386
|
) {
|
|
1309
1387
|
index += 1;
|
|
1310
1388
|
continue;
|
|
@@ -1429,14 +1507,41 @@ function decodeRun(value: unknown): ClientRun {
|
|
|
1429
1507
|
"partialText",
|
|
1430
1508
|
"outcome",
|
|
1431
1509
|
"recovery",
|
|
1510
|
+
"via",
|
|
1432
1511
|
],
|
|
1433
1512
|
"run",
|
|
1434
1513
|
);
|
|
1435
1514
|
// 1 and 2 differ only by the event types a version 2 body may carry, and a
|
|
1436
1515
|
// version 1 body carries a subset of them, so one walk decodes both.
|
|
1437
|
-
if (
|
|
1516
|
+
if (
|
|
1517
|
+
run.schemaVersion !== 1 &&
|
|
1518
|
+
run.schemaVersion !== 2 &&
|
|
1519
|
+
run.schemaVersion !== 3
|
|
1520
|
+
) {
|
|
1438
1521
|
throw new Error("run.schemaVersion is invalid");
|
|
1439
1522
|
}
|
|
1523
|
+
let via: ClientRunV1["via"];
|
|
1524
|
+
if (run.via !== undefined) {
|
|
1525
|
+
if (run.schemaVersion !== 3) {
|
|
1526
|
+
throw new Error("run.via requires schemaVersion 3");
|
|
1527
|
+
}
|
|
1528
|
+
const candidate = record(run.via, "run.via");
|
|
1529
|
+
if (candidate.kind === "bot") {
|
|
1530
|
+
exactKeys(candidate, ["kind", "name", "botId"], "run.via");
|
|
1531
|
+
via = {
|
|
1532
|
+
kind: "bot",
|
|
1533
|
+
name: wireString(candidate, "name", 100, "run.via"),
|
|
1534
|
+
botId: string(candidate, "botId", 128, "run.via"),
|
|
1535
|
+
};
|
|
1536
|
+
} else if (candidate.kind === "voice") {
|
|
1537
|
+
exactKeys(candidate, ["kind", "name"], "run.via");
|
|
1538
|
+
if (candidate.name !== "Voice")
|
|
1539
|
+
throw new Error("run.via.name is invalid");
|
|
1540
|
+
via = { kind: "voice", name: "Voice" };
|
|
1541
|
+
} else {
|
|
1542
|
+
throw new Error("run.via.kind is invalid");
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1440
1545
|
let runId: string;
|
|
1441
1546
|
try {
|
|
1442
1547
|
runId = decodeRunIdV1(string(run, "runId", MAX_RUN_ID_LENGTH, "run"));
|
|
@@ -1507,6 +1612,7 @@ function decodeRun(value: unknown): ClientRun {
|
|
|
1507
1612
|
}
|
|
1508
1613
|
: {}),
|
|
1509
1614
|
...(recovery ? { failure: recovery.message, recovery } : {}),
|
|
1615
|
+
...(via ? { via } : {}),
|
|
1510
1616
|
};
|
|
1511
1617
|
}
|
|
1512
1618
|
|
package/src/shared.ts
CHANGED
|
@@ -98,6 +98,9 @@ export interface WebChatMessage {
|
|
|
98
98
|
| "error"
|
|
99
99
|
| "interrupted"
|
|
100
100
|
| "reconciliation-required";
|
|
101
|
+
via?:
|
|
102
|
+
| { kind: "bot"; name: string; botId: string }
|
|
103
|
+
| { kind: "voice"; name: "Voice" };
|
|
101
104
|
/**
|
|
102
105
|
* True while this line's Turn is admitted but has not started, because the
|
|
103
106
|
* User sent it while the Bot was still on the previous one. The thread
|