@frockbot/plugin-shell 0.3.10 → 0.3.12
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/package.json +34 -32
- package/src/agent.test.ts +66 -0
- package/src/agent.ts +107 -2
- package/src/backend-configuration.test.ts +15 -9
- package/src/backend-package-catalog.ts +75 -26
- package/src/backend-runner.ts +19 -2
- package/src/backend.ts +118 -27
- package/src/client/FrockBotApp.vue +405 -134
- package/src/client/activity-trail.test.ts +205 -0
- package/src/client/activity-trail.ts +227 -0
- package/src/client/index.test.ts +25 -5
- package/src/client/index.ts +191 -47
- package/src/client/model-presentation.test.ts +3 -3
- package/src/client/no-bot-model-label.test.ts +7 -7
- package/src/client/skill-invocation.test.ts +34 -0
- package/src/client/skill-invocation.ts +22 -0
- package/src/client/styles.css +134 -89
- package/src/client/transcript-cache.test.ts +125 -0
- package/src/client/transcript-cache.ts +190 -0
- package/src/compaction-scheduler.test.ts +96 -0
- package/src/compaction-scheduler.ts +108 -0
- package/src/compaction-transcript.test.ts +174 -0
- package/src/compaction.test.ts +596 -0
- package/src/compaction.ts +539 -0
- package/src/focus.test.ts +222 -0
- package/src/focus.ts +93 -0
- package/src/history.ts +86 -8
- package/src/legacy-frock-model-id.test.ts +148 -0
- package/src/notification-id.test.ts +26 -0
- package/src/notification-id.ts +0 -0
- package/src/run-protocol.test.ts +37 -0
- package/src/run-protocol.ts +148 -38
- package/src/settings-links.test.ts +8 -2
- package/src/settings-links.ts +17 -2
- package/src/shared.ts +36 -0
- package/src/unread.ts +23 -1
- package/tsconfig.json +1 -2
package/src/backend.ts
CHANGED
|
@@ -52,6 +52,8 @@ import {
|
|
|
52
52
|
RUN_ADMISSION_FENCE_PREFIX,
|
|
53
53
|
RUN_INDEX_PREFIX,
|
|
54
54
|
RUN_PREFIX,
|
|
55
|
+
CONVERSATION_BUSY_MESSAGE_V1,
|
|
56
|
+
isConversationBusyV1,
|
|
55
57
|
type BotIdentity,
|
|
56
58
|
type BotDurableAuthorityOptions,
|
|
57
59
|
type BotTurnExecutionInput,
|
|
@@ -389,6 +391,7 @@ import {
|
|
|
389
391
|
projectPackageIframeCompositionV1,
|
|
390
392
|
} from "./composition-views.js";
|
|
391
393
|
import { executeBotTurn, executeDirectToolTurn } from "./backend-runner.js";
|
|
394
|
+
import { yieldCompactionWorkV1 } from "./compaction-scheduler.js";
|
|
392
395
|
import {
|
|
393
396
|
shellTerminalRecordsV1,
|
|
394
397
|
supersededTurnRecordsV1,
|
|
@@ -410,6 +413,7 @@ import {
|
|
|
410
413
|
projectClientTurnV1,
|
|
411
414
|
type ClientRunLookupV1,
|
|
412
415
|
type ClientConversationListV1,
|
|
416
|
+
type ClientConversationOutcomeV1,
|
|
413
417
|
type ClientRunListV1,
|
|
414
418
|
type ClientRunStopReceiptV1,
|
|
415
419
|
type ClientRunV1,
|
|
@@ -475,7 +479,7 @@ import { defineBotBackendContribution } from "@frockbot/kernel-contracts/contrib
|
|
|
475
479
|
* is the direction this deployment wants to be wrong in.
|
|
476
480
|
*
|
|
477
481
|
* Today: the in-process foundation provider, and nothing else. Ollama Cloud
|
|
478
|
-
* exposes no provider-bound retrieval (ADR 0010) and neither does
|
|
482
|
+
* exposes no provider-bound retrieval (ADR 0010) and neither does Frock AI.
|
|
479
483
|
*/
|
|
480
484
|
const RECONCILING_PROVIDER_IDS_V1: ReadonlySet<string> = new Set([
|
|
481
485
|
"foundation",
|
|
@@ -588,8 +592,8 @@ export interface BotStateEnv {
|
|
|
588
592
|
MEMORY_INDEX: VectorizeIndex;
|
|
589
593
|
/** The native AI binding consumed through the image Package adapter. */
|
|
590
594
|
AI?: NativeAiBindingV1;
|
|
591
|
-
/** The
|
|
592
|
-
|
|
595
|
+
/** The Frock AI Gateway adapter constructed by the Cloudflare host. */
|
|
596
|
+
FROCK_AI?: {
|
|
593
597
|
autoRoute: string;
|
|
594
598
|
runChatCompletion(
|
|
595
599
|
gatewayModel: string,
|
|
@@ -1135,14 +1139,43 @@ export class ShellBotBackendContribution {
|
|
|
1135
1139
|
if (expired.length > 0) await transaction.delete(expired);
|
|
1136
1140
|
}
|
|
1137
1141
|
|
|
1138
|
-
/**
|
|
1142
|
+
/**
|
|
1143
|
+
* The announcements the Session shows, oldest first.
|
|
1144
|
+
*
|
|
1145
|
+
* Two sources, and deliberately so. A rename or a settled task has no live
|
|
1146
|
+
* Session to be appended to, so it lives in this object's own bounded
|
|
1147
|
+
* announcement log. A compaction (ADR 0030) is already a durable event on
|
|
1148
|
+
* the conversation's session log — appending a second copy of it here would
|
|
1149
|
+
* be two records of one fact — so it is read back from there instead.
|
|
1150
|
+
*/
|
|
1139
1151
|
async listAnnouncements(): Promise<SessionEvent[]> {
|
|
1140
1152
|
const stored = await this.ctx.storage.list<unknown>({
|
|
1141
1153
|
prefix: BOT_ANNOUNCEMENT_PREFIX,
|
|
1142
1154
|
});
|
|
1143
|
-
|
|
1155
|
+
const announcements = [...stored.entries()]
|
|
1144
1156
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
1145
1157
|
.map(([, value]) => decodeSessionEvent(value));
|
|
1158
|
+
const session =
|
|
1159
|
+
(await this.ctx.storage.get<SessionEvent[]>(LATEST_EVENTS_KEY)) ?? [];
|
|
1160
|
+
for (const event of session) {
|
|
1161
|
+
if (event.type === "conversation/compacted") announcements.push(event);
|
|
1162
|
+
}
|
|
1163
|
+
return announcements
|
|
1164
|
+
.sort((left, right) => left.timestamp.localeCompare(right.timestamp))
|
|
1165
|
+
.slice(-BOT_ANNOUNCEMENT_RETENTION);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* The announcements as the transcript reads them, each already carrying the
|
|
1170
|
+
* timestamp of the place it belongs rather than the moment it was written.
|
|
1171
|
+
*/
|
|
1172
|
+
private async projectAnnouncementPage() {
|
|
1173
|
+
const session =
|
|
1174
|
+
(await this.ctx.storage.get<SessionEvent[]>(LATEST_EVENTS_KEY)) ?? [];
|
|
1175
|
+
return projectClientAnnouncementsV1(
|
|
1176
|
+
await this.listAnnouncements(),
|
|
1177
|
+
session,
|
|
1178
|
+
);
|
|
1146
1179
|
}
|
|
1147
1180
|
|
|
1148
1181
|
private async refreshRecoveryAlarm(
|
|
@@ -1158,6 +1191,9 @@ export class ShellBotBackendContribution {
|
|
|
1158
1191
|
}
|
|
1159
1192
|
|
|
1160
1193
|
async run(command: OwnedBotTurnCommand): Promise<ClientTurnV1> {
|
|
1194
|
+
// Before the authority reads the session log, so a compaction detached
|
|
1195
|
+
// from the previous Turn has already handed the log back (ADR 0030).
|
|
1196
|
+
await yieldCompactionWorkV1(command.sessionId);
|
|
1161
1197
|
// Before admission, so the pin this Turn takes already carries whatever the
|
|
1162
1198
|
// User's Applet directory says now.
|
|
1163
1199
|
await this.resolveAppletComposition(
|
|
@@ -1239,18 +1275,7 @@ export class ShellBotBackendContribution {
|
|
|
1239
1275
|
return { schemaVersion: 1, skills: entries };
|
|
1240
1276
|
}
|
|
1241
1277
|
|
|
1242
|
-
/**
|
|
1243
|
-
* The one durable manifest lookup used by mounts, commands, and UI views —
|
|
1244
|
-
* as the **stored document**, byte-for-byte what `manifestHash` was taken
|
|
1245
|
-
* over at authoring time.
|
|
1246
|
-
*
|
|
1247
|
-
* Decoding rebuilds the object (`decodeV5` always writes a `configuration`
|
|
1248
|
-
* key, for one), so a decoded manifest does not canonicalize back to the
|
|
1249
|
-
* recorded hash. Every mount re-verifies that hash
|
|
1250
|
-
* (`botIsolatePackageDescriptorV1`), so the raw document is the only thing
|
|
1251
|
-
* that can be handed to it; callers that want the typed shape decode it
|
|
1252
|
-
* themselves through `readCompositionMemberManifest`.
|
|
1253
|
-
*/
|
|
1278
|
+
/** The two places this Bot's manifests live; see `composition-manifest.ts`. */
|
|
1254
1279
|
private compositionManifestSources(): CompositionManifestSourcesV1 {
|
|
1255
1280
|
return {
|
|
1256
1281
|
stored: (manifestHash) =>
|
|
@@ -1261,6 +1286,16 @@ export class ShellBotBackendContribution {
|
|
|
1261
1286
|
};
|
|
1262
1287
|
}
|
|
1263
1288
|
|
|
1289
|
+
/**
|
|
1290
|
+
* The manifest a **mount** is handed: the stored document, byte-for-byte
|
|
1291
|
+
* what `manifestHash` was taken over at authoring time.
|
|
1292
|
+
*
|
|
1293
|
+
* Decoding rebuilds the object (`decodeV5` always writes a `configuration`
|
|
1294
|
+
* key, for one), so a decoded manifest does not canonicalize back to the
|
|
1295
|
+
* recorded hash. Every mount re-verifies that hash
|
|
1296
|
+
* (`botIsolatePackageDescriptorV1`), so the raw document is the only thing
|
|
1297
|
+
* that can be handed to it.
|
|
1298
|
+
*/
|
|
1264
1299
|
private readCompositionMemberManifestDocument(
|
|
1265
1300
|
member: CompositionMemberV1,
|
|
1266
1301
|
): Promise<unknown | undefined> {
|
|
@@ -1270,6 +1305,10 @@ export class ShellBotBackendContribution {
|
|
|
1270
1305
|
);
|
|
1271
1306
|
}
|
|
1272
1307
|
|
|
1308
|
+
/**
|
|
1309
|
+
* The same manifest as the typed shape, for the callers that are not mounts:
|
|
1310
|
+
* commands and UI views, which read fields rather than re-hash the document.
|
|
1311
|
+
*/
|
|
1273
1312
|
private readCompositionMemberManifest(
|
|
1274
1313
|
member: CompositionMemberV1,
|
|
1275
1314
|
): Promise<FrockBotManifest | undefined> {
|
|
@@ -1590,6 +1629,10 @@ export class ShellBotBackendContribution {
|
|
|
1590
1629
|
private async executeTurn(
|
|
1591
1630
|
input: BotTurnExecutionInput<BotSettingsViewV1>,
|
|
1592
1631
|
): Promise<BotTurnCompletion> {
|
|
1632
|
+
// ADR 0030: a compaction detached from the previous Turn yields to this
|
|
1633
|
+
// one rather than holding it. Free when none is running, and an abort when
|
|
1634
|
+
// one is, so this Turn is the only writer of the session log.
|
|
1635
|
+
await yieldCompactionWorkV1(input.command.sessionId);
|
|
1593
1636
|
const settings = input.configurationSnapshot;
|
|
1594
1637
|
const turn = {
|
|
1595
1638
|
runId: input.command.runId,
|
|
@@ -4761,11 +4804,11 @@ export class ShellBotBackendContribution {
|
|
|
4761
4804
|
bindingPackageId,
|
|
4762
4805
|
effectId,
|
|
4763
4806
|
),
|
|
4764
|
-
...(this.env.
|
|
4807
|
+
...(this.env.FROCK_AI
|
|
4765
4808
|
? {
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
this.env.
|
|
4809
|
+
frockAiAutoRoute: this.env.FROCK_AI.autoRoute,
|
|
4810
|
+
runFrockAiChatCompletion: (gatewayModel, body) =>
|
|
4811
|
+
this.env.FROCK_AI!.runChatCompletion(gatewayModel, body),
|
|
4769
4812
|
}
|
|
4770
4813
|
: {}),
|
|
4771
4814
|
fetch: this.outboundFetch,
|
|
@@ -5386,9 +5429,33 @@ export class ShellBotBackendContribution {
|
|
|
5386
5429
|
index.map((entry) => entry.cursor),
|
|
5387
5430
|
await this.sidebarPreview(storedPreview, index),
|
|
5388
5431
|
failures,
|
|
5432
|
+
await this.isWorking(index[0]?.runId),
|
|
5389
5433
|
);
|
|
5390
5434
|
}
|
|
5391
5435
|
|
|
5436
|
+
/**
|
|
5437
|
+
* Whether the Bot's newest admitted run is still going.
|
|
5438
|
+
*
|
|
5439
|
+
* The sidebar draws this as an activity ring, so somebody in another
|
|
5440
|
+
* conversation can see a Bot working rather than reading a quiet row as a
|
|
5441
|
+
* stalled one. It is the newest run only: a Bot admits one Turn at a time,
|
|
5442
|
+
* so an older run that is somehow still marked running is a reconciliation
|
|
5443
|
+
* problem and not something a ring should report. A read that fails is no
|
|
5444
|
+
* ring — liveness is never worth failing a sidebar poll for.
|
|
5445
|
+
*
|
|
5446
|
+
* The record's `status` is not the test and never was. `resolveRunWorking`
|
|
5447
|
+
* holds the rule — running, inside the Turn deadline, and a Turn the log has
|
|
5448
|
+
* not already closed — and settles the record when it finds one that only
|
|
5449
|
+
* claims to be running, which is why this read is also the repair.
|
|
5450
|
+
*/
|
|
5451
|
+
private async isWorking(runId: string | undefined): Promise<boolean> {
|
|
5452
|
+
try {
|
|
5453
|
+
return await this.authority.resolveRunWorking(runId);
|
|
5454
|
+
} catch {
|
|
5455
|
+
return false;
|
|
5456
|
+
}
|
|
5457
|
+
}
|
|
5458
|
+
|
|
5392
5459
|
/**
|
|
5393
5460
|
* How many stored runs a read will open to recover a missing preview. The
|
|
5394
5461
|
* newest settled chat Turn is almost always the first entry; the bound is
|
|
@@ -5613,6 +5680,16 @@ export class ShellBotBackendContribution {
|
|
|
5613
5680
|
limit: CLIENT_RUN_PAGE_LIMIT + 1,
|
|
5614
5681
|
...(query.before ? { before: query.before } : {}),
|
|
5615
5682
|
});
|
|
5683
|
+
// The open chat draws its own activity ring from whichever run this page
|
|
5684
|
+
// projects as `running`, so it owes the same liveness rule the sidebar row
|
|
5685
|
+
// does — and from the same helper, or the two surfaces disagree about the
|
|
5686
|
+
// same Bot. Only the newest run and the active marker are asked: a Turn
|
|
5687
|
+
// further back cannot be the one anybody is waiting on, and a transcript
|
|
5688
|
+
// read is not the place to walk a Bot's whole history looking for
|
|
5689
|
+
// leftovers.
|
|
5690
|
+
if (!query.before) {
|
|
5691
|
+
await this.isWorking(activeRunId ?? candidates[0]?.runId);
|
|
5692
|
+
}
|
|
5616
5693
|
|
|
5617
5694
|
const selected = new Map<string, { cursor?: string; run: ClientRunV1 }>();
|
|
5618
5695
|
if (activeRunId) {
|
|
@@ -5682,9 +5759,7 @@ export class ShellBotBackendContribution {
|
|
|
5682
5759
|
: { truncated: false },
|
|
5683
5760
|
// Announcements belong to the Session, not to a page of Turns, so only
|
|
5684
5761
|
// the newest page carries them.
|
|
5685
|
-
query.before
|
|
5686
|
-
? []
|
|
5687
|
-
: projectClientAnnouncementsV1(await this.listAnnouncements()),
|
|
5762
|
+
query.before ? [] : await this.projectAnnouncementPage(),
|
|
5688
5763
|
);
|
|
5689
5764
|
if (clientRunListWireBytes(page) > CLIENT_RUN_LIST_MAX_BYTES) {
|
|
5690
5765
|
throw new Error("required run projections exceed the wire byte limit");
|
|
@@ -5715,10 +5790,26 @@ export class ShellBotBackendContribution {
|
|
|
5715
5790
|
*/
|
|
5716
5791
|
async startConversation(
|
|
5717
5792
|
identity: BotIdentity,
|
|
5718
|
-
): Promise<
|
|
5793
|
+
): Promise<ClientConversationOutcomeV1> {
|
|
5719
5794
|
await this.validateIdentity(identity);
|
|
5720
|
-
|
|
5721
|
-
|
|
5795
|
+
try {
|
|
5796
|
+
await this.authority.startConversation(identity);
|
|
5797
|
+
} catch (error) {
|
|
5798
|
+
// The one refusal this can give travels as data. Everything else is a
|
|
5799
|
+
// genuine failure and still throws, so the boundary above answers 500.
|
|
5800
|
+
if (isConversationBusyV1(error)) {
|
|
5801
|
+
return {
|
|
5802
|
+
status: "refused",
|
|
5803
|
+
schemaVersion: 1,
|
|
5804
|
+
reason:
|
|
5805
|
+
error instanceof Error
|
|
5806
|
+
? error.message
|
|
5807
|
+
: CONVERSATION_BUSY_MESSAGE_V1,
|
|
5808
|
+
};
|
|
5809
|
+
}
|
|
5810
|
+
throw error;
|
|
5811
|
+
}
|
|
5812
|
+
return { status: "started", ...(await this.listConversations()) };
|
|
5722
5813
|
}
|
|
5723
5814
|
|
|
5724
5815
|
async lookupRun(input: unknown): Promise<ClientRunLookupV1> {
|