@frockbot/plugin-shell 0.3.9 → 0.3.11
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 +33 -31
- package/src/agent.test.ts +66 -0
- package/src/agent.ts +42 -1
- package/src/backend-configuration.test.ts +10 -2
- package/src/backend-package-catalog.ts +98 -21
- package/src/backend.ts +99 -13
- package/src/client/FrockBotApp.vue +52 -111
- package/src/client/activity-ring.test.ts +89 -0
- package/src/client/activity-ring.ts +94 -0
- package/src/client/index.ts +1 -0
- package/src/client/model-presentation.test.ts +14 -0
- package/src/client/model-presentation.ts +6 -0
- package/src/client/styles.css +39 -93
- package/src/composition-manifest.test.ts +126 -0
- package/src/composition-manifest.ts +55 -0
- package/src/notification-id.test.ts +93 -0
- package/src/notification-id.ts +0 -0
- package/src/run-protocol.ts +13 -2
- package/src/settings-links.ts +6 -0
- package/src/unread.ts +23 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-shell",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"./frockbot.json": "./frockbot.json",
|
|
31
31
|
"./host": "./src/host.ts",
|
|
32
32
|
"./manifest": "./src/manifest.ts",
|
|
33
|
+
"./notification-id": "./src/notification-id.ts",
|
|
33
34
|
"./package.json": "./package.json",
|
|
34
35
|
"./run-cursor": "./src/run-cursor.ts",
|
|
35
36
|
"./run-protocol": "./src/run-protocol.ts",
|
|
@@ -47,41 +48,42 @@
|
|
|
47
48
|
},
|
|
48
49
|
"dependencies": {
|
|
49
50
|
"@cordisjs/plugin-webui": "0.8.2",
|
|
50
|
-
"@frockbot/agent-runtime": "0.3.
|
|
51
|
-
"@frockbot/application-foundation": "0.3.
|
|
52
|
-
"@frockbot/catalog-core": "0.3.
|
|
53
|
-
"@frockbot/client-core": "0.3.
|
|
54
|
-
"@frockbot/client-ui": "0.3.
|
|
55
|
-
"@frockbot/computer-core": "0.3.
|
|
56
|
-
"@frockbot/computer-host-protocol": "0.3.
|
|
57
|
-
"@frockbot/configuration-core": "0.3.
|
|
58
|
-
"@frockbot/connection-core": "0.3.
|
|
59
|
-
"@frockbot/kernel-agent-loop": "0.3.
|
|
60
|
-
"@frockbot/kernel-composition": "0.3.
|
|
61
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
62
|
-
"@frockbot/kernel-do": "0.3.
|
|
63
|
-
"@frockbot/machine-protocol": "0.3.
|
|
64
|
-
"@frockbot/plugin-applets": "0.3.
|
|
65
|
-
"@frockbot/plugin-authoring": "0.3.
|
|
66
|
-
"@frockbot/plugin-bot-template": "0.3.
|
|
67
|
-
"@frockbot/plugin-computer": "0.3.
|
|
68
|
-
"@frockbot/plugin-flock": "0.3.
|
|
69
|
-
"@frockbot/plugin-image": "0.3.
|
|
70
|
-
"@frockbot/plugin-machine-messages": "0.3.
|
|
71
|
-
"@frockbot/plugin-mcp": "0.3.
|
|
72
|
-
"@frockbot/plugin-memory": "0.3.
|
|
73
|
-
"@frockbot/plugin-package-catalog": "0.3.
|
|
74
|
-
"@frockbot/plugin-routines": "0.3.
|
|
75
|
-
"@frockbot/plugin-skills": "0.3.
|
|
76
|
-
"@frockbot/plugin-subagents": "0.3.
|
|
77
|
-
"@frockbot/plugin-user-machine": "0.3.
|
|
78
|
-
"@frockbot/protocol": "0.3.
|
|
51
|
+
"@frockbot/agent-runtime": "0.3.11",
|
|
52
|
+
"@frockbot/application-foundation": "0.3.11",
|
|
53
|
+
"@frockbot/catalog-core": "0.3.11",
|
|
54
|
+
"@frockbot/client-core": "0.3.11",
|
|
55
|
+
"@frockbot/client-ui": "0.3.11",
|
|
56
|
+
"@frockbot/computer-core": "0.3.11",
|
|
57
|
+
"@frockbot/computer-host-protocol": "0.3.11",
|
|
58
|
+
"@frockbot/configuration-core": "0.3.11",
|
|
59
|
+
"@frockbot/connection-core": "0.3.11",
|
|
60
|
+
"@frockbot/kernel-agent-loop": "0.3.11",
|
|
61
|
+
"@frockbot/kernel-composition": "0.3.11",
|
|
62
|
+
"@frockbot/kernel-contracts": "0.3.11",
|
|
63
|
+
"@frockbot/kernel-do": "0.3.11",
|
|
64
|
+
"@frockbot/machine-protocol": "0.3.11",
|
|
65
|
+
"@frockbot/plugin-applets": "0.3.11",
|
|
66
|
+
"@frockbot/plugin-authoring": "0.3.11",
|
|
67
|
+
"@frockbot/plugin-bot-template": "0.3.11",
|
|
68
|
+
"@frockbot/plugin-computer": "0.3.11",
|
|
69
|
+
"@frockbot/plugin-flock": "0.3.11",
|
|
70
|
+
"@frockbot/plugin-image": "0.3.11",
|
|
71
|
+
"@frockbot/plugin-machine-messages": "0.3.11",
|
|
72
|
+
"@frockbot/plugin-mcp": "0.3.11",
|
|
73
|
+
"@frockbot/plugin-memory": "0.3.11",
|
|
74
|
+
"@frockbot/plugin-package-catalog": "0.3.11",
|
|
75
|
+
"@frockbot/plugin-routines": "0.3.11",
|
|
76
|
+
"@frockbot/plugin-skills": "0.3.11",
|
|
77
|
+
"@frockbot/plugin-subagents": "0.3.11",
|
|
78
|
+
"@frockbot/plugin-user-machine": "0.3.11",
|
|
79
|
+
"@frockbot/protocol": "0.3.11",
|
|
79
80
|
"cordis": "4.0.0-rc.8",
|
|
80
81
|
"vue": "3.5.41"
|
|
81
82
|
},
|
|
82
83
|
"devDependencies": {
|
|
83
84
|
"@cloudflare/workers-types": "latest",
|
|
84
|
-
"@frockbot/plugin-
|
|
85
|
+
"@frockbot/plugin-prompt": "0.3.11",
|
|
86
|
+
"@frockbot/plugin-tools": "0.3.11",
|
|
85
87
|
"@types/bun": "1.3.6",
|
|
86
88
|
"@types/node": "26.2.0",
|
|
87
89
|
"@vitejs/plugin-vue": "6.0.8",
|
package/src/agent.test.ts
CHANGED
|
@@ -8,9 +8,12 @@ import {
|
|
|
8
8
|
type ToolExecutionContext,
|
|
9
9
|
type TurnTypeV1,
|
|
10
10
|
} from "@frockbot/kernel-contracts";
|
|
11
|
+
import { SystemPromptRegistry } from "@frockbot/plugin-prompt";
|
|
11
12
|
import { ToolRegistry } from "@frockbot/plugin-tools";
|
|
12
13
|
import { Context } from "cordis";
|
|
13
14
|
import {
|
|
15
|
+
CONVERSATION_PROMPT_SECTION_V1,
|
|
16
|
+
CONVERSATION_PROMPT_TEXT_V1,
|
|
14
17
|
shellAdmissionCeilingV1,
|
|
15
18
|
shellAgentPlugin,
|
|
16
19
|
PARENT_HANDOFF_CAPABILITY_V1,
|
|
@@ -31,6 +34,7 @@ interface Mounted {
|
|
|
31
34
|
async function mount(): Promise<Mounted> {
|
|
32
35
|
const root = new Context();
|
|
33
36
|
await root.plugin(SessionStore);
|
|
37
|
+
await root.plugin(SystemPromptRegistry);
|
|
34
38
|
await root.plugin(ToolRegistry);
|
|
35
39
|
const session = root.sessions.create(SESSION_ID);
|
|
36
40
|
session.appendBatch([
|
|
@@ -382,3 +386,65 @@ describe("wake_parent", () => {
|
|
|
382
386
|
}
|
|
383
387
|
});
|
|
384
388
|
});
|
|
389
|
+
|
|
390
|
+
// The conversational contract is a prompt section and a tool description, and
|
|
391
|
+
// the two have to say the same thing: a model that read one and not the other
|
|
392
|
+
// would have half the rule.
|
|
393
|
+
describe("the conversation prompt section", () => {
|
|
394
|
+
test("is assembled into the system prompt the model reads", async () => {
|
|
395
|
+
const mounted = await mount();
|
|
396
|
+
try {
|
|
397
|
+
const assembled = await mounted.root.systemPrompt.assemble({
|
|
398
|
+
sessionId: SESSION_ID,
|
|
399
|
+
provider: "test",
|
|
400
|
+
model: "test-model",
|
|
401
|
+
turnType: "chat",
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const section = assembled.sections.find(
|
|
405
|
+
(candidate) => candidate.id === CONVERSATION_PROMPT_SECTION_V1,
|
|
406
|
+
);
|
|
407
|
+
expect(section?.text).toBe(CONVERSATION_PROMPT_TEXT_V1);
|
|
408
|
+
expect(assembled.text).toContain(CONVERSATION_PROMPT_TEXT_V1);
|
|
409
|
+
} finally {
|
|
410
|
+
await mounted.dispose();
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("says acknowledge first, then beats, and never the steps between", () => {
|
|
415
|
+
expect(CONVERSATION_PROMPT_TEXT_V1).toContain("On it.");
|
|
416
|
+
expect(CONVERSATION_PROMPT_TEXT_V1).toContain(
|
|
417
|
+
"send only on a real beat: the result, a decision only the user can make, or a blocker",
|
|
418
|
+
);
|
|
419
|
+
expect(CONVERSATION_PROMPT_TEXT_V1).toContain(
|
|
420
|
+
"Never narrate what you are doing",
|
|
421
|
+
);
|
|
422
|
+
expect(CONVERSATION_PROMPT_TEXT_V1).toContain(
|
|
423
|
+
"Never leave a question or a request hanging",
|
|
424
|
+
);
|
|
425
|
+
// Short enough to be read as guidance rather than skimmed as a document.
|
|
426
|
+
expect(CONVERSATION_PROMPT_TEXT_V1.split("\n").length).toBeLessThanOrEqual(
|
|
427
|
+
12,
|
|
428
|
+
);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("matches what the send tool's own description tells the model", async () => {
|
|
432
|
+
const mounted = await mount();
|
|
433
|
+
try {
|
|
434
|
+
const schema = mounted.root.tools
|
|
435
|
+
.schemas({ turnType: "chat" })
|
|
436
|
+
.find((tool) => tool.name === SEND_TO_USER_TOOL_V1);
|
|
437
|
+
const description = schema?.description ?? "";
|
|
438
|
+
|
|
439
|
+
expect(description).toContain("only way to say anything the user sees");
|
|
440
|
+
// When to call it, not only what it does.
|
|
441
|
+
expect(description).toContain("Call it once, immediately");
|
|
442
|
+
expect(description).toContain("Call it again only on a real");
|
|
443
|
+
expect(description).toContain("narrate a step or a tool");
|
|
444
|
+
expect(description).toContain("never end your Turn leaving the");
|
|
445
|
+
expect(description).toContain("keep it short");
|
|
446
|
+
} finally {
|
|
447
|
+
await mounted.dispose();
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
});
|
package/src/agent.ts
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Two tools, and no authority of its own:
|
|
5
5
|
//
|
|
6
|
+
// 0. One prompt section, `conversation`: when to speak and when not to. It
|
|
7
|
+
// is contributed beside the tool so the section and the tool description
|
|
8
|
+
// cannot drift into telling the model two different things.
|
|
9
|
+
//
|
|
6
10
|
// 1. `send_to_user` (legacy alias `send_message`) — parity register row 57b.
|
|
7
11
|
// One tool carrying the typed payload union, admitted on chat turns only,
|
|
8
12
|
// recording each send as `send/to-user` on the durable log. Row 57c: a
|
|
@@ -127,8 +131,38 @@ function sendAcknowledgement(payload: SendToUserPayloadV1): string {
|
|
|
127
131
|
}
|
|
128
132
|
}
|
|
129
133
|
|
|
134
|
+
/**
|
|
135
|
+
* The Bot's conversational contract, in the words the model reads.
|
|
136
|
+
*
|
|
137
|
+
* It lives here rather than in a prompt Package because it is the same rule
|
|
138
|
+
* the send tool's own description states: one place to write it, so the
|
|
139
|
+
* section and the tool cannot drift into telling the model two things. The
|
|
140
|
+
* Shell already owns the voice; it owns how the voice is used.
|
|
141
|
+
*/
|
|
142
|
+
export const CONVERSATION_PROMPT_SECTION_V1 = "conversation";
|
|
143
|
+
/** Ordered after identity (0), before anything a Package contributes. */
|
|
144
|
+
export const CONVERSATION_PROMPT_ORDER_V1 = 1;
|
|
145
|
+
|
|
146
|
+
export const CONVERSATION_PROMPT_TEXT_V1 = [
|
|
147
|
+
"## Talking to the user",
|
|
148
|
+
"",
|
|
149
|
+
"Everything the user sees is a `send_to_user` call; nothing else reaches them.",
|
|
150
|
+
'When a request will take more than a moment, send one short line first — "On it." or "Looking into that." — then go quiet and work.',
|
|
151
|
+
"After that, send only on a real beat: the result, a decision only the user can make, or a blocker you cannot get past.",
|
|
152
|
+
"Never narrate what you are doing, what you are about to do, or which tool you are using.",
|
|
153
|
+
"Never leave a question or a request hanging: before you stop, the user must have the answer, the result, or the reason there isn't one.",
|
|
154
|
+
"When the work is finished, send the result itself, not an account of how you got it.",
|
|
155
|
+
"Keep every message short — a line or two, no preamble and no sign-off.",
|
|
156
|
+
"Don't say the same thing twice.",
|
|
157
|
+
].join("\n");
|
|
158
|
+
|
|
130
159
|
const SEND_TO_USER_DESCRIPTION = [
|
|
131
160
|
"Speak to the user. This is the only way to say anything the user sees.",
|
|
161
|
+
"Call it once, immediately, with one short line when the request will take",
|
|
162
|
+
"more than a moment, then work in silence. Call it again only on a real",
|
|
163
|
+
"beat: the result, a decision only the user can make, or a blocker. Do not",
|
|
164
|
+
"call it to narrate a step or a tool, and never end your Turn leaving the",
|
|
165
|
+
"user's question unanswered. Each call is one message; keep it short.",
|
|
132
166
|
"The payload is one of:",
|
|
133
167
|
'{"type":"text","text":"…"}',
|
|
134
168
|
'{"type":"attachment","url":"https://…","name":"…","mediaType":"…"}',
|
|
@@ -295,6 +329,13 @@ export const shellAgentPlugin: Plugin.Function = (ctx) => {
|
|
|
295
329
|
const userVoice = shellAdmissionCeilingV1(USER_VOICE_CAPABILITY_V1);
|
|
296
330
|
const parentHandoff = shellAdmissionCeilingV1(PARENT_HANDOFF_CAPABILITY_V1);
|
|
297
331
|
const disposers = [
|
|
332
|
+
// The voice and the rules for using it are contributed together, so a
|
|
333
|
+
// Composition that admits the send tool always carries the contract.
|
|
334
|
+
ctx.systemPrompt.register({
|
|
335
|
+
id: CONVERSATION_PROMPT_SECTION_V1,
|
|
336
|
+
order: CONVERSATION_PROMPT_ORDER_V1,
|
|
337
|
+
render: () => CONVERSATION_PROMPT_TEXT_V1,
|
|
338
|
+
}),
|
|
298
339
|
ctx.tools.register(
|
|
299
340
|
createSendToUserTool(SEND_TO_USER_TOOL_V1, ctx.sessions),
|
|
300
341
|
userVoice ? { admissionCeiling: userVoice } : undefined,
|
|
@@ -327,6 +368,6 @@ export const shellAgentPlugin: Plugin.Function = (ctx) => {
|
|
|
327
368
|
for (const dispose of disposers.toReversed()) dispose();
|
|
328
369
|
};
|
|
329
370
|
};
|
|
330
|
-
shellAgentPlugin.inject = ["tools", "sessions"];
|
|
371
|
+
shellAgentPlugin.inject = ["tools", "sessions", "systemPrompt"];
|
|
331
372
|
|
|
332
373
|
export default shellAgentPlugin;
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
import type { PackageSettingDefinition } from "@frockbot/kernel-composition";
|
|
9
9
|
import { createShellBotBackendContribution } from "./backend.js";
|
|
10
10
|
import { createIsolateCapabilityHost } from "./backend-isolate.js";
|
|
11
|
+
import { notificationIdV1 } from "./notification-id.js";
|
|
11
12
|
|
|
12
13
|
class MemoryStorage {
|
|
13
14
|
readonly values = new Map<string, unknown>();
|
|
@@ -600,8 +601,15 @@ describe("generic per-Turn model resolution", () => {
|
|
|
600
601
|
});
|
|
601
602
|
await expect(contribution.listNotifications()).resolves.toEqual([
|
|
602
603
|
expect.objectContaining({
|
|
603
|
-
|
|
604
|
-
|
|
604
|
+
// Colons would fail the acknowledge decoder, so the mint replaces
|
|
605
|
+
// them and folds in a digest of the raw parts; a notification nobody
|
|
606
|
+
// can acknowledge 400s forever.
|
|
607
|
+
notificationId: notificationIdV1(
|
|
608
|
+
"package-connection-unavailable",
|
|
609
|
+
"run-1",
|
|
610
|
+
"bot-authored",
|
|
611
|
+
"flock-ai-ambient",
|
|
612
|
+
),
|
|
605
613
|
title: "Connection unavailable",
|
|
606
614
|
}),
|
|
607
615
|
]);
|
|
@@ -320,7 +320,7 @@ export function createPackageCatalogHost(
|
|
|
320
320
|
change.action === "remove"
|
|
321
321
|
? change.input.packageId
|
|
322
322
|
: change.input.catalogId,
|
|
323
|
-
...(change.action === "remove"
|
|
323
|
+
...(change.action === "remove" || change.input.contentHash === undefined
|
|
324
324
|
? {}
|
|
325
325
|
: { contentHash: change.input.contentHash }),
|
|
326
326
|
}),
|
|
@@ -591,6 +591,13 @@ export function createPackageCatalogHost(
|
|
|
591
591
|
let user: UserSettingsViewV1;
|
|
592
592
|
let base: CompositionGenerationV1;
|
|
593
593
|
let intent: PackageCatalogChangeIntentV1;
|
|
594
|
+
/**
|
|
595
|
+
* The entry names a compiled-in Package that is already required core in
|
|
596
|
+
* this Bot's Composition, so installing it is a User settings change and
|
|
597
|
+
* nothing else. Set on the install path and re-derived on a resumed
|
|
598
|
+
* intent, which carries no bundle hash precisely when this is true.
|
|
599
|
+
*/
|
|
600
|
+
let firstParty = false;
|
|
594
601
|
try {
|
|
595
602
|
user = await options.user.read();
|
|
596
603
|
base = await options.composition.lastKnownGood();
|
|
@@ -611,6 +618,12 @@ export function createPackageCatalogHost(
|
|
|
611
618
|
);
|
|
612
619
|
entry = resumed;
|
|
613
620
|
intent = existingIntent;
|
|
621
|
+
firstParty =
|
|
622
|
+
existingIntent.action !== "remove" &&
|
|
623
|
+
!resumed.bundle &&
|
|
624
|
+
base.members.find(
|
|
625
|
+
(candidate) => candidate.packageId === resumed.packageId,
|
|
626
|
+
)?.provenance.kind === "first-party";
|
|
614
627
|
} else if (action === "remove") {
|
|
615
628
|
const packageId = request.change.input.packageId;
|
|
616
629
|
const member = base.members.find(
|
|
@@ -673,21 +686,46 @@ export function createPackageCatalogHost(
|
|
|
673
686
|
const resolved = await pinnedEntry(request.change.input.catalogId);
|
|
674
687
|
user = resolved.user;
|
|
675
688
|
entry = resolved.entry;
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
689
|
+
// A first-party entry names reviewed compiled-in code and publishes
|
|
690
|
+
// no bundle: there is no artifact to verify and no hash to pin. The
|
|
691
|
+
// Plugins page has always installed such an entry; refusing it here
|
|
692
|
+
// is what made `package_install` by chat disagree with the UI (F4).
|
|
693
|
+
if (entry.bundle) {
|
|
694
|
+
if (entry.bundle.contentHash !== request.change.input.contentHash) {
|
|
695
|
+
throw new Error(
|
|
696
|
+
`Catalog entry "${entry.catalogId}" has content hash "${entry.bundle.contentHash}", not "${request.change.input.contentHash}"`,
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
await verifyArtifact(entry);
|
|
700
|
+
} else if (request.change.input.contentHash !== undefined) {
|
|
682
701
|
throw new Error(
|
|
683
|
-
`Catalog entry "${entry.catalogId}"
|
|
702
|
+
`Catalog entry "${entry.catalogId}" does not carry a Package bundle`,
|
|
684
703
|
);
|
|
685
704
|
}
|
|
686
|
-
await verifyArtifact(entry);
|
|
687
705
|
const member = base.members.find(
|
|
688
706
|
(candidate) => candidate.packageId === entry.packageId,
|
|
689
707
|
);
|
|
690
|
-
|
|
708
|
+
// Installing a first-party entry changes the *User's* settings, not
|
|
709
|
+
// the Composition: the compiled-in Package is already required core
|
|
710
|
+
// in the bootstrap generation (every one of them is), and
|
|
711
|
+
// `assertRequiredCoreSet` refuses any proposal that restates a
|
|
712
|
+
// first-party member with catalog provenance. So "already a member"
|
|
713
|
+
// is the normal state here, not a conflict — the guard below is
|
|
714
|
+
// about a *bundle-backed* entry, whose code really would be added
|
|
715
|
+
// twice.
|
|
716
|
+
firstParty =
|
|
717
|
+
!entry.bundle && member?.provenance.kind === "first-party";
|
|
718
|
+
if (firstParty) {
|
|
719
|
+
// The one integrity check left for a member nothing re-hashes at
|
|
720
|
+
// mount: the entry must name the manifest the application actually
|
|
721
|
+
// compiled in, or the Catalog has drifted from this deployment.
|
|
722
|
+
if (member!.manifestHash !== entry.manifestHash) {
|
|
723
|
+
throw new Error(
|
|
724
|
+
`Catalog entry "${entry.catalogId}" names manifest "${entry.manifestHash}", but this deployment ships "${member!.manifestHash}"`,
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (action === "install" && member && !firstParty) {
|
|
691
729
|
throw new Error(
|
|
692
730
|
`Package "${entry.packageId}" is already in this Bot's Composition`,
|
|
693
731
|
);
|
|
@@ -716,7 +754,7 @@ export function createPackageCatalogHost(
|
|
|
716
754
|
version: entry.version,
|
|
717
755
|
catalogId: entry.catalogId,
|
|
718
756
|
catalogGeneration: resolved.generation,
|
|
719
|
-
contentHash: entry.bundle.contentHash,
|
|
757
|
+
...(entry.bundle ? { contentHash: entry.bundle.contentHash } : {}),
|
|
720
758
|
manifestHash: entry.manifestHash,
|
|
721
759
|
summary:
|
|
722
760
|
request.change.input.summary ??
|
|
@@ -744,16 +782,27 @@ export function createPackageCatalogHost(
|
|
|
744
782
|
// Intent and manifest before the User settings effect. The manifest key
|
|
745
783
|
// is the existing isolate loader seam used by authored Packages.
|
|
746
784
|
if (!existingIntent) {
|
|
785
|
+
const bundle = entry.bundle;
|
|
747
786
|
await options.storage.put({
|
|
748
787
|
[packageCatalogChangeIntentKey(request.effectId)]: intent,
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
788
|
+
// A bundle-less entry stores no manifest document, because it adds
|
|
789
|
+
// no Composition member to mount one for: the compiled-in Package is
|
|
790
|
+
// already required core, running the manifest this deployment
|
|
791
|
+
// shipped. The entry's `manifestHash` was checked against that
|
|
792
|
+
// member's above, so a Catalog that has drifted from this deployment
|
|
793
|
+
// is refused rather than silently recorded.
|
|
794
|
+
...(bundle
|
|
795
|
+
? {
|
|
796
|
+
[authorshipManifestKey(intent.manifestHash)]: {
|
|
797
|
+
schemaVersion: 1,
|
|
798
|
+
manifestHash: intent.manifestHash,
|
|
799
|
+
packageId: intent.packageId,
|
|
800
|
+
version: intent.version,
|
|
801
|
+
manifest: bundle.manifest,
|
|
802
|
+
createdAt: intent.recordedAt,
|
|
803
|
+
} satisfies AuthoredManifestRecordV1,
|
|
804
|
+
}
|
|
805
|
+
: {}),
|
|
757
806
|
});
|
|
758
807
|
existingIntent = intent;
|
|
759
808
|
}
|
|
@@ -776,7 +825,9 @@ export function createPackageCatalogHost(
|
|
|
776
825
|
version: intent.version,
|
|
777
826
|
catalogId: intent.catalogId,
|
|
778
827
|
catalogGeneration: intent.catalogGeneration,
|
|
779
|
-
|
|
828
|
+
...(intent.contentHash === undefined
|
|
829
|
+
? {}
|
|
830
|
+
: { contentHash: intent.contentHash }),
|
|
780
831
|
};
|
|
781
832
|
const receipt = await options.user.execute(command);
|
|
782
833
|
if (receipt.status === "rejected") {
|
|
@@ -785,6 +836,30 @@ export function createPackageCatalogHost(
|
|
|
785
836
|
|
|
786
837
|
const parent =
|
|
787
838
|
(await options.composition.read(intent.baseGenerationId)) ?? base;
|
|
839
|
+
// A first-party entry contributes no member: it is already required core
|
|
840
|
+
// in the bootstrap generation, and restating it with catalog provenance
|
|
841
|
+
// is exactly what `assertRequiredCoreSet` refuses. The install is the
|
|
842
|
+
// User settings command above, which is all the Plugins page does too,
|
|
843
|
+
// so the Composition is left on the generation it was already running.
|
|
844
|
+
if (firstParty) {
|
|
845
|
+
const outcome: PackageCatalogChangeOutcomeRecordV1 = {
|
|
846
|
+
schemaVersion: 1,
|
|
847
|
+
status: "recorded",
|
|
848
|
+
effectId: request.effectId,
|
|
849
|
+
action,
|
|
850
|
+
packageId: intent.packageId,
|
|
851
|
+
displayName: intent.displayName,
|
|
852
|
+
version: intent.version,
|
|
853
|
+
generationId: parent.generationId,
|
|
854
|
+
missingConnectionTypes: intent.missingConnectionTypes,
|
|
855
|
+
recordedAt: intent.recordedAt,
|
|
856
|
+
};
|
|
857
|
+
await options.storage.put({
|
|
858
|
+
[packageCatalogChangeOutcomeKey(request.effectId)]: outcome,
|
|
859
|
+
});
|
|
860
|
+
return recordedOutcome(outcome);
|
|
861
|
+
}
|
|
862
|
+
|
|
788
863
|
const members = parent.members.filter(
|
|
789
864
|
(member) => member.packageId !== intent.packageId,
|
|
790
865
|
);
|
|
@@ -850,7 +925,9 @@ export function createPackageCatalogHost(
|
|
|
850
925
|
packageId: intent.packageId,
|
|
851
926
|
displayName: intent.displayName,
|
|
852
927
|
...(action === "remove" ? {} : { version: intent.version }),
|
|
853
|
-
...(action === "remove"
|
|
928
|
+
...(action === "remove" || intent.contentHash === undefined
|
|
929
|
+
? {}
|
|
930
|
+
: { contentHash: intent.contentHash }),
|
|
854
931
|
generationId: generation.generationId,
|
|
855
932
|
missingConnectionTypes: intent.missingConnectionTypes,
|
|
856
933
|
recordedAt: intent.recordedAt,
|
package/src/backend.ts
CHANGED
|
@@ -415,6 +415,12 @@ import {
|
|
|
415
415
|
type ClientRunV1,
|
|
416
416
|
type ClientTurnV1,
|
|
417
417
|
} from "./run-protocol.js";
|
|
418
|
+
import { notificationIdV1 } from "./notification-id.js";
|
|
419
|
+
import {
|
|
420
|
+
type CompositionManifestSourcesV1,
|
|
421
|
+
compositionMemberManifestDocumentV1,
|
|
422
|
+
compositionMemberManifestV1,
|
|
423
|
+
} from "./composition-manifest.js";
|
|
418
424
|
import {
|
|
419
425
|
BOT_DEBUG_DEFAULT_RUN_LIMIT_V1,
|
|
420
426
|
BOT_DEBUG_EVENT_BYTES_V1,
|
|
@@ -1233,15 +1239,47 @@ export class ShellBotBackendContribution {
|
|
|
1233
1239
|
return { schemaVersion: 1, skills: entries };
|
|
1234
1240
|
}
|
|
1235
1241
|
|
|
1236
|
-
/** The
|
|
1237
|
-
private
|
|
1242
|
+
/** The two places this Bot's manifests live; see `composition-manifest.ts`. */
|
|
1243
|
+
private compositionManifestSources(): CompositionManifestSourcesV1 {
|
|
1244
|
+
return {
|
|
1245
|
+
stored: (manifestHash) =>
|
|
1246
|
+
this.ctx.storage.get<AuthoredManifestRecordV1>(
|
|
1247
|
+
authorshipManifestKey(manifestHash),
|
|
1248
|
+
),
|
|
1249
|
+
application: (member) => this.readApplicationMemberManifest(member),
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/**
|
|
1254
|
+
* The manifest a **mount** is handed: the stored document, byte-for-byte
|
|
1255
|
+
* what `manifestHash` was taken over at authoring time.
|
|
1256
|
+
*
|
|
1257
|
+
* Decoding rebuilds the object (`decodeV5` always writes a `configuration`
|
|
1258
|
+
* key, for one), so a decoded manifest does not canonicalize back to the
|
|
1259
|
+
* recorded hash. Every mount re-verifies that hash
|
|
1260
|
+
* (`botIsolatePackageDescriptorV1`), so the raw document is the only thing
|
|
1261
|
+
* that can be handed to it.
|
|
1262
|
+
*/
|
|
1263
|
+
private readCompositionMemberManifestDocument(
|
|
1264
|
+
member: CompositionMemberV1,
|
|
1265
|
+
): Promise<unknown | undefined> {
|
|
1266
|
+
return compositionMemberManifestDocumentV1(
|
|
1267
|
+
member,
|
|
1268
|
+
this.compositionManifestSources(),
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* The same manifest as the typed shape, for the callers that are not mounts:
|
|
1274
|
+
* commands and UI views, which read fields rather than re-hash the document.
|
|
1275
|
+
*/
|
|
1276
|
+
private readCompositionMemberManifest(
|
|
1238
1277
|
member: CompositionMemberV1,
|
|
1239
1278
|
): Promise<FrockBotManifest | undefined> {
|
|
1240
|
-
|
|
1241
|
-
|
|
1279
|
+
return compositionMemberManifestV1(
|
|
1280
|
+
member,
|
|
1281
|
+
this.compositionManifestSources(),
|
|
1242
1282
|
);
|
|
1243
|
-
if (stored) return decodeFrockBotManifest(stored.manifest);
|
|
1244
|
-
return await this.readApplicationMemberManifest(member);
|
|
1245
1283
|
}
|
|
1246
1284
|
|
|
1247
1285
|
/**
|
|
@@ -1270,6 +1308,19 @@ export class ShellBotBackendContribution {
|
|
|
1270
1308
|
return declared.manifest;
|
|
1271
1309
|
}
|
|
1272
1310
|
|
|
1311
|
+
/** The stored manifest document a mount hashes, or a modelled failure. */
|
|
1312
|
+
private async requireCompositionMemberManifestDocument(
|
|
1313
|
+
member: CompositionMemberV1,
|
|
1314
|
+
): Promise<unknown> {
|
|
1315
|
+
const document = await this.readCompositionMemberManifestDocument(member);
|
|
1316
|
+
if (document === undefined) {
|
|
1317
|
+
throw new Error(
|
|
1318
|
+
`package "${member.packageId}" manifest "${member.manifestHash}" is unavailable`,
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
return document;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1273
1324
|
private async requireCompositionMemberManifest(
|
|
1274
1325
|
member: CompositionMemberV1,
|
|
1275
1326
|
): Promise<FrockBotManifest> {
|
|
@@ -1832,7 +1883,11 @@ export class ShellBotBackendContribution {
|
|
|
1832
1883
|
fallback: CompositionGenerationV1,
|
|
1833
1884
|
): Promise<void> {
|
|
1834
1885
|
await this.authority.recordNotification({
|
|
1835
|
-
notificationId:
|
|
1886
|
+
notificationId: notificationIdV1(
|
|
1887
|
+
"composition-failure",
|
|
1888
|
+
failure.generationId,
|
|
1889
|
+
failure.attempt,
|
|
1890
|
+
),
|
|
1836
1891
|
runId,
|
|
1837
1892
|
createdAt: failure.at,
|
|
1838
1893
|
title: `${settings.profile.name} kept its last working Packages`,
|
|
@@ -1883,7 +1938,8 @@ export class ShellBotBackendContribution {
|
|
|
1883
1938
|
artifacts,
|
|
1884
1939
|
this.bundledPackageArtifacts,
|
|
1885
1940
|
),
|
|
1886
|
-
manifestFor: (member) =>
|
|
1941
|
+
manifestFor: (member) =>
|
|
1942
|
+
this.requireCompositionMemberManifestDocument(member),
|
|
1887
1943
|
capabilitiesFor: (member) =>
|
|
1888
1944
|
mintCapabilities({
|
|
1889
1945
|
props: {
|
|
@@ -2692,7 +2748,12 @@ export class ShellBotBackendContribution {
|
|
|
2692
2748
|
);
|
|
2693
2749
|
if (!connection?.generation) {
|
|
2694
2750
|
await this.authority.recordNotification({
|
|
2695
|
-
notificationId:
|
|
2751
|
+
notificationId: notificationIdV1(
|
|
2752
|
+
"package-connection-unavailable",
|
|
2753
|
+
input.runId,
|
|
2754
|
+
input.packageId,
|
|
2755
|
+
input.request,
|
|
2756
|
+
),
|
|
2696
2757
|
runId: input.runId,
|
|
2697
2758
|
createdAt: new Date().toISOString(),
|
|
2698
2759
|
title: "Connection unavailable",
|
|
@@ -2720,7 +2781,11 @@ export class ShellBotBackendContribution {
|
|
|
2720
2781
|
}
|
|
2721
2782
|
const request = decodeIsolateNotificationRequestV1(input.request);
|
|
2722
2783
|
await this.authority.recordNotification({
|
|
2723
|
-
notificationId:
|
|
2784
|
+
notificationId: notificationIdV1(
|
|
2785
|
+
"package",
|
|
2786
|
+
input.packageId,
|
|
2787
|
+
request.notificationId,
|
|
2788
|
+
),
|
|
2724
2789
|
runId: input.runId,
|
|
2725
2790
|
createdAt: new Date().toISOString(),
|
|
2726
2791
|
title: request.title,
|
|
@@ -3107,7 +3172,7 @@ export class ShellBotBackendContribution {
|
|
|
3107
3172
|
await this.authority.recordNotification({
|
|
3108
3173
|
// The same id shape the completion path uses, so one firing is one
|
|
3109
3174
|
// intent however many times the alarm retries it.
|
|
3110
|
-
notificationId:
|
|
3175
|
+
notificationId: notificationIdV1("routine-failed", fire.fireId),
|
|
3111
3176
|
runId: fire.fireId,
|
|
3112
3177
|
createdAt: new Date().toISOString(),
|
|
3113
3178
|
title: `${settings.profile.name} could not run a Routine`,
|
|
@@ -3660,7 +3725,7 @@ export class ShellBotBackendContribution {
|
|
|
3660
3725
|
}
|
|
3661
3726
|
if (!settings.notifications.enabled) return;
|
|
3662
3727
|
await this.authority.recordNotification({
|
|
3663
|
-
notificationId:
|
|
3728
|
+
notificationId: notificationIdV1("task-settled", task.taskId),
|
|
3664
3729
|
runId: task.taskId,
|
|
3665
3730
|
createdAt: at,
|
|
3666
3731
|
title: `${settings.profile.name} finished a subagent task`,
|
|
@@ -5324,9 +5389,30 @@ export class ShellBotBackendContribution {
|
|
|
5324
5389
|
index.map((entry) => entry.cursor),
|
|
5325
5390
|
await this.sidebarPreview(storedPreview, index),
|
|
5326
5391
|
failures,
|
|
5392
|
+
await this.isWorking(index[0]?.runId),
|
|
5327
5393
|
);
|
|
5328
5394
|
}
|
|
5329
5395
|
|
|
5396
|
+
/**
|
|
5397
|
+
* Whether the Bot's newest admitted run is still going.
|
|
5398
|
+
*
|
|
5399
|
+
* The sidebar draws this as an activity ring, so somebody in another
|
|
5400
|
+
* conversation can see a Bot working rather than reading a quiet row as a
|
|
5401
|
+
* stalled one. It is the newest run only: a Bot admits one Turn at a time,
|
|
5402
|
+
* so an older run that is somehow still marked running is a reconciliation
|
|
5403
|
+
* problem and not something a ring should report. A read that fails is no
|
|
5404
|
+
* ring — liveness is never worth failing a sidebar poll for.
|
|
5405
|
+
*/
|
|
5406
|
+
private async isWorking(runId: string | undefined): Promise<boolean> {
|
|
5407
|
+
if (runId === undefined) return false;
|
|
5408
|
+
try {
|
|
5409
|
+
const run = await this.authority.readRun(runId);
|
|
5410
|
+
return run?.status === "running";
|
|
5411
|
+
} catch {
|
|
5412
|
+
return false;
|
|
5413
|
+
}
|
|
5414
|
+
}
|
|
5415
|
+
|
|
5330
5416
|
/**
|
|
5331
5417
|
* How many stored runs a read will open to recover a missing preview. The
|
|
5332
5418
|
* newest settled chat Turn is almost always the first entry; the bound is
|
|
@@ -5464,7 +5550,7 @@ export class ShellBotBackendContribution {
|
|
|
5464
5550
|
// `automation_completion_inbox` row".
|
|
5465
5551
|
if (handoff === undefined) return undefined;
|
|
5466
5552
|
return {
|
|
5467
|
-
notificationId:
|
|
5553
|
+
notificationId: notificationIdV1("routine-wake", result.runId),
|
|
5468
5554
|
runId: result.runId,
|
|
5469
5555
|
createdAt: new Date().toISOString(),
|
|
5470
5556
|
title: `${settings.profile.name} finished a Routine`,
|