@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/backend-flock.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// The Bot Durable Object's half of the Bot self-management seam.
|
|
2
2
|
//
|
|
3
|
-
// The Flock Package offers a Bot
|
|
4
|
-
// User's flock. This module decides, for one admitted
|
|
5
|
-
// those
|
|
6
|
-
//
|
|
7
|
-
// command,
|
|
8
|
-
//
|
|
3
|
+
// The Flock Package offers a Bot self-management and direct messaging over its
|
|
4
|
+
// own identity and its User's flock. This module decides, for one admitted
|
|
5
|
+
// Turn, what provenance those effects record and which authorities they may
|
|
6
|
+
// reach. The profile write is the Bot Durable Object's own configuration
|
|
7
|
+
// command, create is the User Durable Object's `bot/create`, and messaging
|
|
8
|
+
// admits an agent Turn through the target Bot's Durable Object.
|
|
9
9
|
//
|
|
10
10
|
// AUTHORITY. "Self-modification never widens authority." The host handed to
|
|
11
|
-
// the Package exposes
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
11
|
+
// the Package exposes only narrow authority calls. The `botId` on every
|
|
12
|
+
// mutation is fixed here rather than taken from the model's arguments. A Bot
|
|
13
|
+
// cannot address another Bot's settings through this seam; a message target is
|
|
14
|
+
// instead resolved from the same User's Flock directory before admission.
|
|
15
15
|
//
|
|
16
16
|
// HIBERNATION. Nothing here reaches the Computer registry, a Computer
|
|
17
17
|
// provider, or a Sprite: identity is Durable Object state, so self-management
|
|
@@ -26,7 +26,9 @@ import type {
|
|
|
26
26
|
CreateBotCommandV1,
|
|
27
27
|
FlockReceiptV1,
|
|
28
28
|
FlockSelfRuntimeHostV1,
|
|
29
|
+
BotMessageOutcomeV1,
|
|
29
30
|
} from "@frockbot/plugin-flock/agent";
|
|
31
|
+
import type { AgentTurnSlotReceiptV1 } from "@frockbot/plugin-flock/quota";
|
|
30
32
|
|
|
31
33
|
/** The Bot and User whose identity a Turn may change. */
|
|
32
34
|
export interface BotSelfManagementIdentity {
|
|
@@ -39,6 +41,9 @@ export interface BotSelfManagementTurn {
|
|
|
39
41
|
runId: string;
|
|
40
42
|
turnId: string;
|
|
41
43
|
sessionId: string;
|
|
44
|
+
/** This Turn's pinned profile name, stable across effect recovery. */
|
|
45
|
+
fromBotName: string;
|
|
46
|
+
inboundAgent?: FlockSelfRuntimeHostV1["inboundAgent"];
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
/**
|
|
@@ -57,6 +62,51 @@ export interface BotSelfManagementAuthorities {
|
|
|
57
62
|
userId: string,
|
|
58
63
|
command: CreateBotCommandV1,
|
|
59
64
|
): Promise<FlockReceiptV1>;
|
|
65
|
+
reserveAgentTurn(request: {
|
|
66
|
+
schemaVersion: 1;
|
|
67
|
+
userId: string;
|
|
68
|
+
requesterId: string;
|
|
69
|
+
runId: string;
|
|
70
|
+
reservedAt: string;
|
|
71
|
+
}): Promise<AgentTurnSlotReceiptV1>;
|
|
72
|
+
releaseAgentTurn(request: {
|
|
73
|
+
schemaVersion: 1;
|
|
74
|
+
userId: string;
|
|
75
|
+
requesterId: string;
|
|
76
|
+
runId: string;
|
|
77
|
+
}): Promise<void>;
|
|
78
|
+
runAgent(request: {
|
|
79
|
+
schemaVersion: 1;
|
|
80
|
+
userId: string;
|
|
81
|
+
botId: string;
|
|
82
|
+
command: {
|
|
83
|
+
runId: string;
|
|
84
|
+
sessionId: string;
|
|
85
|
+
acceptedAt: string;
|
|
86
|
+
text: string;
|
|
87
|
+
source: {
|
|
88
|
+
kind: "bot";
|
|
89
|
+
fromBotId: string;
|
|
90
|
+
fromBotName: string;
|
|
91
|
+
messageId: string;
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
}): Promise<{ text: string }>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function agentRunIdV1(
|
|
98
|
+
identity: BotSelfManagementIdentity,
|
|
99
|
+
targetBotId: string,
|
|
100
|
+
effectId: string,
|
|
101
|
+
): Promise<string> {
|
|
102
|
+
const bytes = new TextEncoder().encode(
|
|
103
|
+
`${identity.userId}\u0000${identity.botId}\u0000${targetBotId}\u0000${effectId}`,
|
|
104
|
+
);
|
|
105
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
106
|
+
const hex = [...new Uint8Array(digest)]
|
|
107
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
108
|
+
.join("");
|
|
109
|
+
return `agent-${hex.slice(0, 32)}`;
|
|
60
110
|
}
|
|
61
111
|
|
|
62
112
|
/**
|
|
@@ -92,5 +142,64 @@ export function createBotSelfManagementHost(
|
|
|
92
142
|
},
|
|
93
143
|
listBots: () => authorities.listBots(identity.userId),
|
|
94
144
|
createBot: (command) => authorities.createBot(identity.userId, command),
|
|
145
|
+
...(turn.inboundAgent ? { inboundAgent: turn.inboundAgent } : {}),
|
|
146
|
+
messageBot: async (request): Promise<BotMessageOutcomeV1> => {
|
|
147
|
+
if (request.targetBotId === identity.botId) {
|
|
148
|
+
throw new Error("a Bot cannot message itself");
|
|
149
|
+
}
|
|
150
|
+
const directory = await authorities.listBots(identity.userId);
|
|
151
|
+
const target = directory.bots.find(
|
|
152
|
+
(bot) => bot.botId === request.targetBotId,
|
|
153
|
+
);
|
|
154
|
+
if (!target)
|
|
155
|
+
throw new Error("the target Bot is not in this User's flock");
|
|
156
|
+
const runId = await agentRunIdV1(
|
|
157
|
+
identity,
|
|
158
|
+
request.targetBotId,
|
|
159
|
+
request.effectId,
|
|
160
|
+
);
|
|
161
|
+
const reservation = await authorities.reserveAgentTurn({
|
|
162
|
+
schemaVersion: 1,
|
|
163
|
+
userId: identity.userId,
|
|
164
|
+
requesterId: identity.botId,
|
|
165
|
+
runId,
|
|
166
|
+
reservedAt: new Date().toISOString(),
|
|
167
|
+
});
|
|
168
|
+
if (reservation.status === "refused") {
|
|
169
|
+
throw new Error(reservation.reason);
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const turnResult = await authorities.runAgent({
|
|
173
|
+
schemaVersion: 1,
|
|
174
|
+
userId: identity.userId,
|
|
175
|
+
botId: request.targetBotId,
|
|
176
|
+
command: {
|
|
177
|
+
runId,
|
|
178
|
+
sessionId: `${identity.userId}:${request.targetBotId}`,
|
|
179
|
+
acceptedAt: new Date().toISOString(),
|
|
180
|
+
text: request.message,
|
|
181
|
+
source: {
|
|
182
|
+
kind: "bot",
|
|
183
|
+
fromBotId: identity.botId,
|
|
184
|
+
fromBotName: turn.fromBotName,
|
|
185
|
+
messageId: runId,
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
return {
|
|
190
|
+
targetBotId: request.targetBotId,
|
|
191
|
+
targetBotName: target.initialName,
|
|
192
|
+
runId,
|
|
193
|
+
text: turnResult.text,
|
|
194
|
+
};
|
|
195
|
+
} finally {
|
|
196
|
+
await authorities.releaseAgentTurn({
|
|
197
|
+
schemaVersion: 1,
|
|
198
|
+
userId: identity.userId,
|
|
199
|
+
requesterId: identity.botId,
|
|
200
|
+
runId,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
},
|
|
95
204
|
};
|
|
96
205
|
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type UserSettingsViewV1,
|
|
10
10
|
} from "@frockbot/configuration-core";
|
|
11
11
|
import { compileFoundationApplication } from "@frockbot/application-foundation/runtime";
|
|
12
|
+
import { SessionEventLog } from "@frockbot/kernel-do";
|
|
12
13
|
import { createShellBotBackendContribution } from "./backend.js";
|
|
13
14
|
import {
|
|
14
15
|
botTurnCommandFingerprintV1,
|
|
@@ -291,10 +292,23 @@ describe("Bot recovery", () => {
|
|
|
291
292
|
},
|
|
292
293
|
]);
|
|
293
294
|
expect(settledEffects).toHaveLength(2);
|
|
295
|
+
const eventLog = new SessionEventLog(storage);
|
|
294
296
|
for (const runId of ["ollama-run-1", "ollama-run-2"]) {
|
|
295
|
-
const run = await storage.get<
|
|
297
|
+
const run = await storage.get<
|
|
298
|
+
Omit<StoredRun, "events"> & {
|
|
299
|
+
eventRange: { startSeq: number; endSeq: number };
|
|
300
|
+
}
|
|
301
|
+
>(`run:${runId}`);
|
|
302
|
+
expect(run).not.toHaveProperty("events");
|
|
303
|
+
const events = run
|
|
304
|
+
? await eventLog.readRange(
|
|
305
|
+
"user-1:primary",
|
|
306
|
+
run.eventRange.startSeq,
|
|
307
|
+
run.eventRange.endSeq,
|
|
308
|
+
)
|
|
309
|
+
: [];
|
|
296
310
|
expect(
|
|
297
|
-
|
|
311
|
+
events.find((event) => event.type === "model/request"),
|
|
298
312
|
).toMatchObject({
|
|
299
313
|
request: {
|
|
300
314
|
provider: "ollama-cloud",
|
|
@@ -594,7 +608,7 @@ describe("Bot recovery", () => {
|
|
|
594
608
|
schemaVersion: 1,
|
|
595
609
|
runs: [
|
|
596
610
|
expect.objectContaining({
|
|
597
|
-
schemaVersion:
|
|
611
|
+
schemaVersion: 3,
|
|
598
612
|
runId: "run-1",
|
|
599
613
|
status: "completed",
|
|
600
614
|
outcome: { type: "completed", text: "Durable reply" },
|
package/src/backend-runner.ts
CHANGED
package/src/backend.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
decodeIsolateWorkspaceListRequestV1,
|
|
10
10
|
decodeIsolateWorkspacePathV1,
|
|
11
11
|
decodeIsolateWorkspaceWriteRequestV1,
|
|
12
|
+
decodeSendToUserPayloadV1,
|
|
12
13
|
decodeWorkspacePathV1,
|
|
13
14
|
decodeWorkspaceRootV1,
|
|
14
15
|
decodeSessionEvent,
|
|
@@ -46,12 +47,13 @@ import {
|
|
|
46
47
|
ACTIVE_RUN_KEY,
|
|
47
48
|
BotDurableAuthority,
|
|
48
49
|
IDENTITY_KEY,
|
|
49
|
-
LATEST_EVENTS_KEY,
|
|
50
50
|
NOTIFICATION_PREFIX,
|
|
51
51
|
RECOVERY_ALARM_DELAY_MS,
|
|
52
52
|
RUN_ADMISSION_FENCE_PREFIX,
|
|
53
53
|
RUN_INDEX_PREFIX,
|
|
54
54
|
RUN_PREFIX,
|
|
55
|
+
SessionEventLog,
|
|
56
|
+
storedRunRecordV2,
|
|
55
57
|
CONVERSATION_BUSY_MESSAGE_V1,
|
|
56
58
|
isConversationBusyV1,
|
|
57
59
|
type BotIdentity,
|
|
@@ -133,6 +135,7 @@ import {
|
|
|
133
135
|
import {
|
|
134
136
|
createAppletCapabilityHostV1,
|
|
135
137
|
createAppletInstanceBindingV1,
|
|
138
|
+
APPLET_DIST_FILES_V1,
|
|
136
139
|
appletRpcSnapshotV1 as rpcJsonSnapshotV1,
|
|
137
140
|
resolveAppletCompositionV1,
|
|
138
141
|
type AppletCapabilityHostV1,
|
|
@@ -252,6 +255,7 @@ import {
|
|
|
252
255
|
decodeSubagentSlotReceiptV1,
|
|
253
256
|
type SubagentSlotBinding,
|
|
254
257
|
} from "@frockbot/plugin-subagents/quota";
|
|
258
|
+
import { decodeAgentTurnSlotReceiptV1 } from "@frockbot/plugin-flock/quota";
|
|
255
259
|
import {
|
|
256
260
|
subagentModelCatalogV1,
|
|
257
261
|
type SubagentModelOptionV1,
|
|
@@ -405,6 +409,7 @@ import {
|
|
|
405
409
|
decodeClientRunLookupQueryV1,
|
|
406
410
|
decodeClientRunListQueryV1,
|
|
407
411
|
decodeClientRunStopCommandV1,
|
|
412
|
+
decodeClientTurnV1,
|
|
408
413
|
isVisibleRunV1,
|
|
409
414
|
projectClientRunLookupV1,
|
|
410
415
|
projectClientRunV1,
|
|
@@ -666,6 +671,16 @@ export interface ShellBotBackendHost {
|
|
|
666
671
|
scheduledWorkInFlight?(): boolean;
|
|
667
672
|
deferScheduledWork?(transaction: DurableObjectTransaction): Promise<void>;
|
|
668
673
|
settleScheduledWork?(): Promise<void>;
|
|
674
|
+
/**
|
|
675
|
+
* A derived accounting projection after `turn/end` is durable. The host
|
|
676
|
+
* queues it before delivery, so failure never changes the Turn's outcome.
|
|
677
|
+
*/
|
|
678
|
+
recordSettledUsage?(input: {
|
|
679
|
+
botId: string;
|
|
680
|
+
runId: string;
|
|
681
|
+
turn: number;
|
|
682
|
+
events: readonly SessionEvent[];
|
|
683
|
+
}): Promise<void>;
|
|
669
684
|
}
|
|
670
685
|
|
|
671
686
|
/** The narrow storage seam the Bot's announcement log is written through. */
|
|
@@ -768,6 +783,7 @@ export class ShellBotBackendContribution {
|
|
|
768
783
|
private readonly hostScheduledWorkInFlight?: ShellBotBackendHost["scheduledWorkInFlight"];
|
|
769
784
|
private readonly hostDeferScheduledWork?: ShellBotBackendHost["deferScheduledWork"];
|
|
770
785
|
private readonly hostSettleScheduledWork?: ShellBotBackendHost["settleScheduledWork"];
|
|
786
|
+
private readonly recordSettledUsage?: ShellBotBackendHost["recordSettledUsage"];
|
|
771
787
|
|
|
772
788
|
constructor(host: ShellBotBackendHost) {
|
|
773
789
|
this.ctx = host.state;
|
|
@@ -783,6 +799,7 @@ export class ShellBotBackendContribution {
|
|
|
783
799
|
this.hostScheduledWorkInFlight = host.scheduledWorkInFlight;
|
|
784
800
|
this.hostDeferScheduledWork = host.deferScheduledWork;
|
|
785
801
|
this.hostSettleScheduledWork = host.settleScheduledWork;
|
|
802
|
+
this.recordSettledUsage = host.recordSettledUsage;
|
|
786
803
|
const routines = createBotRoutines(
|
|
787
804
|
host.state.storage,
|
|
788
805
|
createBotRoutineHookMinter(
|
|
@@ -1156,8 +1173,10 @@ export class ShellBotBackendContribution {
|
|
|
1156
1173
|
const announcements = [...stored.entries()]
|
|
1157
1174
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
1158
1175
|
.map(([, value]) => decodeSessionEvent(value));
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1176
|
+
const sessionId = await this.authority.readConversationSessionId();
|
|
1177
|
+
const session = sessionId
|
|
1178
|
+
? await this.authority.readSessionEvents(sessionId)
|
|
1179
|
+
: [];
|
|
1161
1180
|
for (const event of session) {
|
|
1162
1181
|
if (event.type === "conversation/compacted") announcements.push(event);
|
|
1163
1182
|
}
|
|
@@ -1171,8 +1190,10 @@ export class ShellBotBackendContribution {
|
|
|
1171
1190
|
* timestamp of the place it belongs rather than the moment it was written.
|
|
1172
1191
|
*/
|
|
1173
1192
|
private async projectAnnouncementPage() {
|
|
1174
|
-
const
|
|
1175
|
-
|
|
1193
|
+
const sessionId = await this.authority.readConversationSessionId();
|
|
1194
|
+
const session = sessionId
|
|
1195
|
+
? await this.authority.readSessionEvents(sessionId)
|
|
1196
|
+
: [];
|
|
1176
1197
|
return projectClientAnnouncementsV1(
|
|
1177
1198
|
await this.listAnnouncements(),
|
|
1178
1199
|
session,
|
|
@@ -1640,6 +1661,9 @@ export class ShellBotBackendContribution {
|
|
|
1640
1661
|
// One admitted Turn is one run; the Turn ordinal lives in the session log.
|
|
1641
1662
|
turnId: input.command.runId,
|
|
1642
1663
|
sessionId: input.command.sessionId,
|
|
1664
|
+
// The admitted configuration snapshot is durable, so a recovered
|
|
1665
|
+
// bot_message reuses the same sender display name in its target command.
|
|
1666
|
+
fromBotName: settings.profile.name,
|
|
1643
1667
|
// The pin this Turn was admitted under, and the type it was admitted as.
|
|
1644
1668
|
// A subagent dispatched from here runs on this generation, and the model
|
|
1645
1669
|
// catalog it is offered is narrowed by this turn type.
|
|
@@ -1655,6 +1679,17 @@ export class ShellBotBackendContribution {
|
|
|
1655
1679
|
...(input.command.origin?.kind === "subagent"
|
|
1656
1680
|
? { subagentTaskId: input.command.origin.taskId }
|
|
1657
1681
|
: {}),
|
|
1682
|
+
...(input.command.origin?.kind === "bot"
|
|
1683
|
+
? {
|
|
1684
|
+
inboundAgent: {
|
|
1685
|
+
kind: "bot" as const,
|
|
1686
|
+
fromBotId: input.command.origin.fromBotId,
|
|
1687
|
+
fromBotName: input.command.origin.fromBotName,
|
|
1688
|
+
},
|
|
1689
|
+
}
|
|
1690
|
+
: input.command.origin?.kind === "voice"
|
|
1691
|
+
? { inboundAgent: { kind: "voice" as const } }
|
|
1692
|
+
: {}),
|
|
1658
1693
|
};
|
|
1659
1694
|
let mountedRoot: ShellMountedComposition["root"] | undefined;
|
|
1660
1695
|
let mountedGeneration: CompositionGenerationV1 | undefined;
|
|
@@ -1724,6 +1759,16 @@ export class ShellBotBackendContribution {
|
|
|
1724
1759
|
input.command.sessionId,
|
|
1725
1760
|
effect,
|
|
1726
1761
|
),
|
|
1762
|
+
...(this.recordSettledUsage
|
|
1763
|
+
? {
|
|
1764
|
+
onTurnStopping: (settled) =>
|
|
1765
|
+
this.recordSettledUsage!({
|
|
1766
|
+
botId: input.identity.botId,
|
|
1767
|
+
runId: input.command.runId,
|
|
1768
|
+
...settled,
|
|
1769
|
+
}),
|
|
1770
|
+
}
|
|
1771
|
+
: {}),
|
|
1727
1772
|
...(isolate ? { isolate } : {}),
|
|
1728
1773
|
...(appletRouting ? { applets: appletRouting } : {}),
|
|
1729
1774
|
}).mount(mounting, signal);
|
|
@@ -2409,7 +2454,7 @@ export class ShellBotBackendContribution {
|
|
|
2409
2454
|
// User with no Computer assignment has no root to pull, and the Bot that
|
|
2410
2455
|
// just built on its Computer has it open already.
|
|
2411
2456
|
syncSourceRootNow: active
|
|
2412
|
-
? async () => {
|
|
2457
|
+
? async (appletId) => {
|
|
2413
2458
|
const root = active.mounted.runtime.root as unknown as {
|
|
2414
2459
|
computers?: ComputerRegistry;
|
|
2415
2460
|
sessions: typeof active.mounted.runtime.root.sessions;
|
|
@@ -2432,6 +2477,9 @@ export class ShellBotBackendContribution {
|
|
|
2432
2477
|
sessionId: active.sessionId,
|
|
2433
2478
|
turn,
|
|
2434
2479
|
root: appletsSourceRootV1(identity.userId),
|
|
2480
|
+
requiredPaths: APPLET_DIST_FILES_V1.map(
|
|
2481
|
+
(path) => `${appletId}/${path}`,
|
|
2482
|
+
),
|
|
2435
2483
|
signal: active.signal,
|
|
2436
2484
|
});
|
|
2437
2485
|
}
|
|
@@ -3246,6 +3294,22 @@ export class ShellBotBackendContribution {
|
|
|
3246
3294
|
};
|
|
3247
3295
|
}
|
|
3248
3296
|
|
|
3297
|
+
/** Narrow RPC for the User-wide agent-lane concurrency lease. */
|
|
3298
|
+
private agentTurnSlots(identity: BotIdentity) {
|
|
3299
|
+
const id = this.env.USER_CONFIGURATIONS.idFromName(identity.userId);
|
|
3300
|
+
const rpc = this.env.USER_CONFIGURATIONS.get(id) as unknown as {
|
|
3301
|
+
reserveAgentTurnSlot(input: unknown): Promise<unknown>;
|
|
3302
|
+
releaseAgentTurnSlot(input: unknown): Promise<unknown>;
|
|
3303
|
+
};
|
|
3304
|
+
return {
|
|
3305
|
+
reserve: async (request: unknown) =>
|
|
3306
|
+
decodeAgentTurnSlotReceiptV1(await rpc.reserveAgentTurnSlot(request)),
|
|
3307
|
+
release: async (request: unknown) => {
|
|
3308
|
+
await rpc.releaseAgentTurnSlot(request);
|
|
3309
|
+
},
|
|
3310
|
+
};
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3249
3313
|
/**
|
|
3250
3314
|
* The User-wide `desktop-gui` lease, held at the Computer host.
|
|
3251
3315
|
*
|
|
@@ -4328,6 +4392,7 @@ export class ShellBotBackendContribution {
|
|
|
4328
4392
|
runId: string;
|
|
4329
4393
|
turnId: string;
|
|
4330
4394
|
sessionId: string;
|
|
4395
|
+
fromBotName: string;
|
|
4331
4396
|
/**
|
|
4332
4397
|
* The generation this Turn pinned, and the type it was admitted as. A
|
|
4333
4398
|
* dispatched subagent runs on the generation its parent pinned, and the
|
|
@@ -4517,6 +4582,39 @@ export class ShellBotBackendContribution {
|
|
|
4517
4582
|
this.userConfiguration(identity).listBots(userId),
|
|
4518
4583
|
createBot: (userId, command) =>
|
|
4519
4584
|
this.userConfiguration(identity).createBot(userId, command),
|
|
4585
|
+
reserveAgentTurn: (request) =>
|
|
4586
|
+
this.agentTurnSlots(identity).reserve(request),
|
|
4587
|
+
releaseAgentTurn: (request) =>
|
|
4588
|
+
this.agentTurnSlots(identity).release(request),
|
|
4589
|
+
runAgent: async (request) => {
|
|
4590
|
+
if (!this.env.BOT_STATES) {
|
|
4591
|
+
throw new Error("Bot-to-Bot messaging is unavailable");
|
|
4592
|
+
}
|
|
4593
|
+
const id = this.env.BOT_STATES.idFromName(
|
|
4594
|
+
`${request.userId}:${request.botId}`,
|
|
4595
|
+
);
|
|
4596
|
+
const rpc = this.env.BOT_STATES.get(id) as unknown as {
|
|
4597
|
+
runAgent(input: unknown): Promise<unknown>;
|
|
4598
|
+
};
|
|
4599
|
+
const completed = decodeClientTurnV1(
|
|
4600
|
+
structuredClone(await rpc.runAgent(request)),
|
|
4601
|
+
);
|
|
4602
|
+
let sentText: string | undefined;
|
|
4603
|
+
for (const event of completed.events) {
|
|
4604
|
+
if (event.type !== "send/to-user") continue;
|
|
4605
|
+
const payload = decodeSendToUserPayloadV1(
|
|
4606
|
+
event.payload,
|
|
4607
|
+
"agent send/to-user payload",
|
|
4608
|
+
);
|
|
4609
|
+
if (payload.type === "text") {
|
|
4610
|
+
sentText = payload.text;
|
|
4611
|
+
break;
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
return {
|
|
4615
|
+
text: sentText ?? completed.text,
|
|
4616
|
+
};
|
|
4617
|
+
},
|
|
4520
4618
|
}),
|
|
4521
4619
|
}
|
|
4522
4620
|
: {}),
|
|
@@ -5951,10 +6049,13 @@ export class ShellBotBackendContribution {
|
|
|
5951
6049
|
let budget = BOT_DEBUG_EVENT_BYTES_V1;
|
|
5952
6050
|
const runs: BotDebugRunV1[] = [];
|
|
5953
6051
|
for (const candidate of candidates) {
|
|
5954
|
-
const
|
|
5955
|
-
|
|
6052
|
+
const projected = await this.authority.readRunEventProjections(
|
|
6053
|
+
candidate.runId,
|
|
6054
|
+
);
|
|
6055
|
+
if (!projected) continue;
|
|
6056
|
+
const { run: stored } = projected;
|
|
5956
6057
|
const bounded = includeEvents
|
|
5957
|
-
? boundDebugEventsV1(
|
|
6058
|
+
? boundDebugEventsV1(projected.events, budget)
|
|
5958
6059
|
: undefined;
|
|
5959
6060
|
if (bounded) budget = Math.max(0, budget - bounded.spent);
|
|
5960
6061
|
runs.push({
|
|
@@ -5967,7 +6068,7 @@ export class ShellBotBackendContribution {
|
|
|
5967
6068
|
commandFingerprint: stored.commandFingerprint,
|
|
5968
6069
|
compositionGenerationId: stored.compositionGenerationId,
|
|
5969
6070
|
previousEventCount: stored.previousEventCount,
|
|
5970
|
-
eventCount:
|
|
6071
|
+
eventCount: projected.eventCount,
|
|
5971
6072
|
...(stored.responseText === undefined
|
|
5972
6073
|
? {}
|
|
5973
6074
|
: { responseText: stored.responseText }),
|
|
@@ -6328,7 +6429,24 @@ export class ShellBotBackendContribution {
|
|
|
6328
6429
|
transaction.get<BotIdentity>(IDENTITY_KEY),
|
|
6329
6430
|
transaction.get<unknown>(`${RUN_PREFIX}${runId}`),
|
|
6330
6431
|
]);
|
|
6331
|
-
const
|
|
6432
|
+
const storedRun = optionalStoredRun(candidate);
|
|
6433
|
+
let run = storedRun;
|
|
6434
|
+
if (storedRun?.eventRange) {
|
|
6435
|
+
const events = await new SessionEventLog(transaction).readRange(
|
|
6436
|
+
storedRun.sessionId,
|
|
6437
|
+
storedRun.eventRange.startSeq,
|
|
6438
|
+
storedRun.eventRange.endSeq,
|
|
6439
|
+
);
|
|
6440
|
+
if (
|
|
6441
|
+
events.length !==
|
|
6442
|
+
storedRun.eventRange.endSeq - storedRun.eventRange.startSeq
|
|
6443
|
+
) {
|
|
6444
|
+
throw new Error(
|
|
6445
|
+
`run "${storedRun.runId}" has an incomplete event range`,
|
|
6446
|
+
);
|
|
6447
|
+
}
|
|
6448
|
+
run = requireStoredRunV1({ ...storedRun, events });
|
|
6449
|
+
}
|
|
6332
6450
|
if (
|
|
6333
6451
|
activeRunId !== runId ||
|
|
6334
6452
|
!run ||
|
|
@@ -6388,7 +6506,10 @@ export class ShellBotBackendContribution {
|
|
|
6388
6506
|
{ kind: effect.kind, effectId: effect.effectId, outcome },
|
|
6389
6507
|
],
|
|
6390
6508
|
} satisfies StoredRun);
|
|
6391
|
-
await transaction.put(
|
|
6509
|
+
await transaction.put(
|
|
6510
|
+
`${RUN_PREFIX}${runId}`,
|
|
6511
|
+
structuredClone(storedRunRecordV2(next)),
|
|
6512
|
+
);
|
|
6392
6513
|
return outcome === "admitted";
|
|
6393
6514
|
});
|
|
6394
6515
|
}
|
|
@@ -1490,7 +1490,12 @@ function handleComposerKeydown(event: KeyboardEvent): void {
|
|
|
1490
1490
|
</div>
|
|
1491
1491
|
</div>
|
|
1492
1492
|
</template>
|
|
1493
|
-
<div v-else class="message-
|
|
1493
|
+
<div v-else class="message-user-column">
|
|
1494
|
+
<div class="message-bubble">{{ message.text }}</div>
|
|
1495
|
+
<span v-if="message.via" class="message-via">
|
|
1496
|
+
via {{ message.via.name }}
|
|
1497
|
+
</span>
|
|
1498
|
+
</div>
|
|
1494
1499
|
</article>
|
|
1495
1500
|
<!--
|
|
1496
1501
|
The working row: the Bot's own avatar on its own line at the end of
|
package/src/client/index.test.ts
CHANGED
|
@@ -95,6 +95,40 @@ afterEach(() => {
|
|
|
95
95
|
}
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
+
test("a degraded Computer sync is a notice beneath the Turn once", () => {
|
|
99
|
+
const state: Pick<
|
|
100
|
+
FrockBotWebData,
|
|
101
|
+
"messages" | "activeRunId" | "runningRunId" | "activeRun"
|
|
102
|
+
> = { messages: [] };
|
|
103
|
+
|
|
104
|
+
projectDurableRuns(
|
|
105
|
+
state,
|
|
106
|
+
[],
|
|
107
|
+
[
|
|
108
|
+
{
|
|
109
|
+
runId: "run-sync",
|
|
110
|
+
input: "build the applet",
|
|
111
|
+
status: "completed",
|
|
112
|
+
responseText: "The Applet is ready.",
|
|
113
|
+
events: [
|
|
114
|
+
{
|
|
115
|
+
type: "computer/sync",
|
|
116
|
+
status: "degraded",
|
|
117
|
+
message: "Excluded 1 reproducible Workspace item from sync.",
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(state.messages).toHaveLength(2);
|
|
125
|
+
expect(state.messages[1]).toMatchObject({
|
|
126
|
+
role: "assistant",
|
|
127
|
+
text: "The Applet is ready.",
|
|
128
|
+
notice: "Excluded 1 reproducible Workspace item from sync.",
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
98
132
|
describe("application manifest protocol", () => {
|
|
99
133
|
const emptyManifest = {
|
|
100
134
|
schemaVersion: 1,
|
|
@@ -1451,6 +1485,33 @@ describe("active durable Turn projection", () => {
|
|
|
1451
1485
|
expect(state.messages[1]).toMatchObject({ text: "", status: "streaming" });
|
|
1452
1486
|
});
|
|
1453
1487
|
|
|
1488
|
+
test("keeps the source marker on an agent-origin user bubble", () => {
|
|
1489
|
+
const state: Pick<
|
|
1490
|
+
FrockBotWebData,
|
|
1491
|
+
"messages" | "activeRunId" | "activeRun"
|
|
1492
|
+
> = { messages: [] };
|
|
1493
|
+
projectDurableRuns(
|
|
1494
|
+
state,
|
|
1495
|
+
[],
|
|
1496
|
+
[
|
|
1497
|
+
{
|
|
1498
|
+
runId: "run-agent",
|
|
1499
|
+
input: "What changed?",
|
|
1500
|
+
events: [],
|
|
1501
|
+
status: "completed",
|
|
1502
|
+
responseText: "The answer",
|
|
1503
|
+
via: { kind: "bot", name: "Researcher", botId: "researcher" },
|
|
1504
|
+
},
|
|
1505
|
+
],
|
|
1506
|
+
);
|
|
1507
|
+
|
|
1508
|
+
expect(state.messages[0]).toMatchObject({
|
|
1509
|
+
role: "user",
|
|
1510
|
+
text: "What changed?",
|
|
1511
|
+
via: { kind: "bot", name: "Researcher" },
|
|
1512
|
+
});
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1454
1515
|
test("projects dispatched subagents as chips, and skips one it cannot draw", () => {
|
|
1455
1516
|
const state: Pick<
|
|
1456
1517
|
FrockBotWebData,
|