@frockbot/plugin-shell 0.3.5 → 0.3.7
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 +31 -31
- package/src/backend-authoring.test.ts +72 -0
- package/src/backend-authoring.ts +82 -16
- package/src/backend-configuration.test.ts +6 -4
- package/src/backend-package-catalog.test.ts +23 -3
- package/src/backend-package-catalog.ts +7 -1
- package/src/backend-recovery-integration.test.ts +25 -32
- package/src/backend.ts +71 -6
- package/src/client/AppletCanvas.vue +3 -5
- package/src/client/FrockBotApp.vue +176 -106
- package/src/client/PackageIframeHost.vue +15 -6
- package/src/client/index.test.ts +101 -12
- package/src/client/index.ts +191 -59
- package/src/client/model-presentation.test.ts +4 -2
- package/src/client/model-presentation.ts +2 -2
- package/src/client/styles.css +15 -1
- package/src/connection-return.test.ts +55 -0
- package/src/history.test.ts +52 -0
- package/src/history.ts +79 -1
- package/src/run-protocol.ts +112 -5
- package/src/shared.ts +65 -0
package/src/history.test.ts
CHANGED
|
@@ -78,6 +78,58 @@ function scoped(events: SessionEvent[]): LlmMessage[] {
|
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
function scopedWithBudget(events: SessionEvent[], budget: number) {
|
|
82
|
+
return turnScopedMessagesV1({
|
|
83
|
+
events,
|
|
84
|
+
messages: derive(events),
|
|
85
|
+
pointer: automationParentPointerV1,
|
|
86
|
+
sessionId: "bot:scout",
|
|
87
|
+
budget,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
describe("one request carries a bounded amount of history", () => {
|
|
92
|
+
test("keeps the current Turn whole and drops the oldest, with a notice", () => {
|
|
93
|
+
const events = log([
|
|
94
|
+
...turn(1, "chat", "the oldest thing", "first reply"),
|
|
95
|
+
...turn(2, "chat", "the middle thing", "second reply"),
|
|
96
|
+
...turn(3, "chat", "the newest thing", ""),
|
|
97
|
+
]);
|
|
98
|
+
// Room for the current Turn and one older one, not for all three.
|
|
99
|
+
const messages = scopedWithBudget(events, 200);
|
|
100
|
+
|
|
101
|
+
const contents = messages.map((message) => message.content);
|
|
102
|
+
expect(contents).toContain("the newest thing");
|
|
103
|
+
expect(contents.join(" ")).not.toContain("the oldest thing");
|
|
104
|
+
expect(contents[0]).toContain("not included here");
|
|
105
|
+
expect(contents[0]).toContain("1 Turn");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("carries everything when it fits, and says nothing about omission", () => {
|
|
109
|
+
const events = log([
|
|
110
|
+
...turn(1, "chat", "morning", "hello"),
|
|
111
|
+
...turn(2, "chat", "anything new?", ""),
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
expect(
|
|
115
|
+
scopedWithBudget(events, 100_000).map((message) => message.content),
|
|
116
|
+
).toEqual(["morning", "hello", "anything new?", ""]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("keeps the current Turn even when it alone exceeds the budget", () => {
|
|
120
|
+
const events = log([
|
|
121
|
+
...turn(1, "chat", "old", "older"),
|
|
122
|
+
...turn(2, "chat", "x".repeat(500), ""),
|
|
123
|
+
]);
|
|
124
|
+
const messages = scopedWithBudget(events, 50);
|
|
125
|
+
|
|
126
|
+
// A Turn is never split: dropping the user message and keeping the reply
|
|
127
|
+
// would be a malformed request, so the current Turn survives whole.
|
|
128
|
+
expect(messages.at(-2)!.content).toBe("x".repeat(500));
|
|
129
|
+
expect(messages[0]!.content).toContain("not included here");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
81
133
|
describe("turn-scoped prompt history", () => {
|
|
82
134
|
test("a chat Turn sees only the Turns admitted as chat", () => {
|
|
83
135
|
const events = log([
|
package/src/history.ts
CHANGED
|
@@ -68,6 +68,76 @@ export interface TurnScopedMessagesInputV1 {
|
|
|
68
68
|
/** The parent-transcript pointer, used only on a non-chat Turn. */
|
|
69
69
|
pointer(input: { sessionId: string; chatTurns: number }): string;
|
|
70
70
|
sessionId: string;
|
|
71
|
+
/**
|
|
72
|
+
* How many characters of history one request may carry. The current Turn is
|
|
73
|
+
* always whole; older Turns fill what is left. Absent means the default.
|
|
74
|
+
*/
|
|
75
|
+
budget?: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* How much conversation one model request carries.
|
|
80
|
+
*
|
|
81
|
+
* A number in characters, not tokens: this is a Package policy bound whose job
|
|
82
|
+
* is to stop a request growing without limit, and it does not need to agree
|
|
83
|
+
* with any provider's tokenizer to do that. Roughly 150k characters is well
|
|
84
|
+
* inside every model FrockBot resolves today while being far more history than
|
|
85
|
+
* any conversation needs.
|
|
86
|
+
*/
|
|
87
|
+
export const CHAT_HISTORY_BUDGET_CHARS_V1 = 150_000;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The line that stands where the dropped Turns were.
|
|
91
|
+
*
|
|
92
|
+
* It is said plainly, because a model that cannot see the beginning of a
|
|
93
|
+
* conversation and is not told so will confidently answer as though it had.
|
|
94
|
+
*/
|
|
95
|
+
export function omittedHistoryNoticeV1(turns: number): string {
|
|
96
|
+
return `Earlier in this conversation there ${turns === 1 ? "was 1 Turn" : `were ${turns} Turns`} that are not included here. They are not summarised: if you need something from them, say so or search your memory rather than guessing.`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function messageChars(message: LlmMessage): number {
|
|
100
|
+
return JSON.stringify(message).length;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Narrows history to a character budget, oldest Turns first.
|
|
105
|
+
*
|
|
106
|
+
* Eviction is by whole Turn on purpose. A tool result whose call has been
|
|
107
|
+
* dropped is a malformed request to every provider, and a Turn is the
|
|
108
|
+
* smallest unit that always holds both.
|
|
109
|
+
*/
|
|
110
|
+
function budgetedMessagesV1(
|
|
111
|
+
messages: readonly LlmMessage[],
|
|
112
|
+
turns: readonly number[],
|
|
113
|
+
current: number,
|
|
114
|
+
budget: number,
|
|
115
|
+
): LlmMessage[] {
|
|
116
|
+
const total = messages.reduce(
|
|
117
|
+
(sum, message) => sum + messageChars(message),
|
|
118
|
+
0,
|
|
119
|
+
);
|
|
120
|
+
if (total <= budget) return [...messages];
|
|
121
|
+
const spendByTurn = new Map<number, number>();
|
|
122
|
+
for (const [index, message] of messages.entries()) {
|
|
123
|
+
const turn = turns[index]!;
|
|
124
|
+
spendByTurn.set(turn, (spendByTurn.get(turn) ?? 0) + messageChars(message));
|
|
125
|
+
}
|
|
126
|
+
const ordered = [...spendByTurn.keys()].sort((left, right) => right - left);
|
|
127
|
+
const kept = new Set<number>([current]);
|
|
128
|
+
let spent = spendByTurn.get(current) ?? 0;
|
|
129
|
+
for (const turn of ordered) {
|
|
130
|
+
if (turn === current) continue;
|
|
131
|
+
const cost = spendByTurn.get(turn) ?? 0;
|
|
132
|
+
if (spent + cost > budget) break;
|
|
133
|
+
kept.add(turn);
|
|
134
|
+
spent += cost;
|
|
135
|
+
}
|
|
136
|
+
const dropped = ordered.filter((turn) => !kept.has(turn)).length;
|
|
137
|
+
const narrowed = messages.filter((_, index) => kept.has(turns[index]!));
|
|
138
|
+
return dropped === 0
|
|
139
|
+
? narrowed
|
|
140
|
+
: [{ role: "user", content: omittedHistoryNoticeV1(dropped) }, ...narrowed];
|
|
71
141
|
}
|
|
72
142
|
|
|
73
143
|
/**
|
|
@@ -92,7 +162,15 @@ export function turnScopedMessagesV1(
|
|
|
92
162
|
const current = currentTurnV1(input.events);
|
|
93
163
|
const chatTurn = (turn: number) => (types.get(turn) ?? "chat") === "chat";
|
|
94
164
|
if (chatTurn(current)) {
|
|
95
|
-
|
|
165
|
+
const conversation = input.messages.filter((_, index) =>
|
|
166
|
+
chatTurn(turns[index]!),
|
|
167
|
+
);
|
|
168
|
+
return budgetedMessagesV1(
|
|
169
|
+
conversation,
|
|
170
|
+
turns.filter((turn) => chatTurn(turn)),
|
|
171
|
+
current,
|
|
172
|
+
input.budget ?? CHAT_HISTORY_BUDGET_CHARS_V1,
|
|
173
|
+
);
|
|
96
174
|
}
|
|
97
175
|
const own = input.messages.filter((_, index) => turns[index] === current);
|
|
98
176
|
const chatTurns = new Set(
|
package/src/run-protocol.ts
CHANGED
|
@@ -36,6 +36,8 @@ const MAX_NOTIFICATION_TITLE_BYTES = 512;
|
|
|
36
36
|
const MAX_NOTIFICATION_BODY_BYTES = 2_000;
|
|
37
37
|
const MAX_CLIENT_TURN_BYTES = 256_000;
|
|
38
38
|
const MAX_CURSOR_LENGTH = 320;
|
|
39
|
+
/** A conversation is named by its Session id, which the kernel bounds. */
|
|
40
|
+
const MAX_SESSION_ID_LENGTH = 320;
|
|
39
41
|
const MAX_TASK_DESCRIPTION_BYTES = 800;
|
|
40
42
|
const MAX_TASK_MODEL_BYTES = 512;
|
|
41
43
|
export const CLIENT_RUN_PAGE_LIMIT = 32;
|
|
@@ -179,7 +181,12 @@ export interface ClientDynamicToolCallInputV1 {
|
|
|
179
181
|
|
|
180
182
|
export type ClientRunOutcomeV1 =
|
|
181
183
|
| { type: "completed"; text: string }
|
|
182
|
-
|
|
184
|
+
/**
|
|
185
|
+
* A Turn that broke keeps what it had already said, for the same reason a
|
|
186
|
+
* stopped one does: the words arrived, the person read them, and replacing
|
|
187
|
+
* them with a notice would rewrite what they watched happen (ADR 0028).
|
|
188
|
+
*/
|
|
189
|
+
| { type: "failed"; message: string; text?: string }
|
|
183
190
|
/**
|
|
184
191
|
* A Turn a Stop or a later message ended keeps what it had already said:
|
|
185
192
|
* `text` is that partial answer, and `message` is the line saying why it
|
|
@@ -246,9 +253,85 @@ export interface ClientRunListV1 {
|
|
|
246
253
|
announcements?: ClientAnnouncementV1[];
|
|
247
254
|
}
|
|
248
255
|
|
|
256
|
+
/**
|
|
257
|
+
* One conversation a Bot has had.
|
|
258
|
+
*
|
|
259
|
+
* A Bot holds one conversation at a time and keeps the ones before it: the
|
|
260
|
+
* transcript shows the current one, and an earlier one is still readable.
|
|
261
|
+
*/
|
|
262
|
+
export interface ClientConversationV1 {
|
|
263
|
+
schemaVersion: 1;
|
|
264
|
+
/** The Session id this conversation's Turns recorded. */
|
|
265
|
+
conversationId: string;
|
|
266
|
+
ordinal: number;
|
|
267
|
+
startedAt: string;
|
|
268
|
+
/** Absent while this is the conversation the Bot is on. */
|
|
269
|
+
endedAt?: string;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export interface ClientConversationListV1 {
|
|
273
|
+
schemaVersion: 1;
|
|
274
|
+
/** Newest first; the first entry is the conversation the Bot is on. */
|
|
275
|
+
conversations: ClientConversationV1[];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function decodeClientConversationListV1(
|
|
279
|
+
input: unknown,
|
|
280
|
+
): ClientConversationListV1 {
|
|
281
|
+
const list = record(input, "conversation list");
|
|
282
|
+
exactKeys(list, ["schemaVersion", "conversations"], "conversation list");
|
|
283
|
+
if (list.schemaVersion !== 1) {
|
|
284
|
+
throw new Error("conversation list.schemaVersion is invalid");
|
|
285
|
+
}
|
|
286
|
+
if (!Array.isArray(list.conversations)) {
|
|
287
|
+
throw new Error("conversation list.conversations is invalid");
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
schemaVersion: 1,
|
|
291
|
+
conversations: list.conversations.map((entry) => {
|
|
292
|
+
const conversation = record(entry, "conversation");
|
|
293
|
+
exactKeys(
|
|
294
|
+
conversation,
|
|
295
|
+
["schemaVersion", "conversationId", "ordinal", "startedAt", "endedAt"],
|
|
296
|
+
"conversation",
|
|
297
|
+
);
|
|
298
|
+
if (conversation.schemaVersion !== 1) {
|
|
299
|
+
throw new Error("conversation.schemaVersion is invalid");
|
|
300
|
+
}
|
|
301
|
+
if (
|
|
302
|
+
typeof conversation.ordinal !== "number" ||
|
|
303
|
+
!Number.isSafeInteger(conversation.ordinal) ||
|
|
304
|
+
conversation.ordinal < 1
|
|
305
|
+
) {
|
|
306
|
+
throw new Error("conversation.ordinal is invalid");
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
schemaVersion: 1 as const,
|
|
310
|
+
conversationId: string(
|
|
311
|
+
conversation,
|
|
312
|
+
"conversationId",
|
|
313
|
+
MAX_SESSION_ID_LENGTH,
|
|
314
|
+
"conversation",
|
|
315
|
+
),
|
|
316
|
+
ordinal: conversation.ordinal,
|
|
317
|
+
startedAt: string(conversation, "startedAt", 64, "conversation"),
|
|
318
|
+
...(conversation.endedAt === undefined
|
|
319
|
+
? {}
|
|
320
|
+
: { endedAt: string(conversation, "endedAt", 64, "conversation") }),
|
|
321
|
+
};
|
|
322
|
+
}),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
249
326
|
export interface ClientRunListQueryV1 {
|
|
250
327
|
schemaVersion: 1;
|
|
251
328
|
before?: string;
|
|
329
|
+
/**
|
|
330
|
+
* The conversation to read. Absent means the one the Bot is on now, which
|
|
331
|
+
* is what the transcript shows; an earlier conversation is named by the
|
|
332
|
+
* Session id `listConversations` gave for it.
|
|
333
|
+
*/
|
|
334
|
+
conversationId?: string;
|
|
252
335
|
}
|
|
253
336
|
|
|
254
337
|
export interface ClientTurnCommandV1 {
|
|
@@ -673,6 +756,7 @@ export function projectClientRunV1(run: StoredRun): ClientRunV1 {
|
|
|
673
756
|
run.failure ?? "Agent request failed.",
|
|
674
757
|
MAX_FAILURE_BYTES,
|
|
675
758
|
),
|
|
759
|
+
...interruptedOutcomeTextV1(run),
|
|
676
760
|
} satisfies ClientRunOutcomeV1)
|
|
677
761
|
: status === "cancelled"
|
|
678
762
|
? ({
|
|
@@ -1121,10 +1205,11 @@ function decodeOutcome(
|
|
|
1121
1205
|
};
|
|
1122
1206
|
}
|
|
1123
1207
|
if (outcome.type === "failed" && runStatus === "failed") {
|
|
1124
|
-
exactKeys(outcome, ["type", "message"], "run.outcome");
|
|
1208
|
+
exactKeys(outcome, ["type", "message", "text"], "run.outcome");
|
|
1125
1209
|
return {
|
|
1126
1210
|
type: "failed",
|
|
1127
1211
|
message: wireString(outcome, "message", MAX_FAILURE_BYTES, "run.outcome"),
|
|
1212
|
+
...decodeInterruptedTextV1(outcome),
|
|
1128
1213
|
};
|
|
1129
1214
|
}
|
|
1130
1215
|
if (outcome.type === "cancelled" && runStatus === "cancelled") {
|
|
@@ -1249,7 +1334,12 @@ function decodeRun(value: unknown): ClientRun {
|
|
|
1249
1334
|
...(stopRequestedAt ? { stopRequestedAt } : {}),
|
|
1250
1335
|
...(run.queued === true ? { queued: true as const } : {}),
|
|
1251
1336
|
...(outcome?.type === "completed" ? { responseText: outcome.text } : {}),
|
|
1252
|
-
...(outcome?.type === "failed"
|
|
1337
|
+
...(outcome?.type === "failed"
|
|
1338
|
+
? {
|
|
1339
|
+
failure: outcome.message,
|
|
1340
|
+
...(outcome.text ? { responseText: outcome.text } : {}),
|
|
1341
|
+
}
|
|
1342
|
+
: {}),
|
|
1253
1343
|
...(outcome?.type === "cancelled" || outcome?.type === "superseded"
|
|
1254
1344
|
? {
|
|
1255
1345
|
failure: outcome.message,
|
|
@@ -1373,7 +1463,11 @@ export function decodeClientRunListQueryV1(
|
|
|
1373
1463
|
input: unknown,
|
|
1374
1464
|
): ClientRunListQueryV1 {
|
|
1375
1465
|
const query = record(input, "run list query");
|
|
1376
|
-
exactKeys(
|
|
1466
|
+
exactKeys(
|
|
1467
|
+
query,
|
|
1468
|
+
["schemaVersion", "before", "conversationId"],
|
|
1469
|
+
"run list query",
|
|
1470
|
+
);
|
|
1377
1471
|
if (query.schemaVersion !== 1) {
|
|
1378
1472
|
throw new Error("run list query.schemaVersion is invalid");
|
|
1379
1473
|
}
|
|
@@ -1388,7 +1482,20 @@ export function decodeClientRunListQueryV1(
|
|
|
1388
1482
|
throw new Error("run list query.before is invalid");
|
|
1389
1483
|
}
|
|
1390
1484
|
}
|
|
1391
|
-
|
|
1485
|
+
const conversationId =
|
|
1486
|
+
query.conversationId === undefined
|
|
1487
|
+
? undefined
|
|
1488
|
+
: string(
|
|
1489
|
+
query,
|
|
1490
|
+
"conversationId",
|
|
1491
|
+
MAX_SESSION_ID_LENGTH,
|
|
1492
|
+
"run list query",
|
|
1493
|
+
);
|
|
1494
|
+
return {
|
|
1495
|
+
schemaVersion: 1,
|
|
1496
|
+
...(before ? { before } : {}),
|
|
1497
|
+
...(conversationId ? { conversationId } : {}),
|
|
1498
|
+
};
|
|
1392
1499
|
}
|
|
1393
1500
|
|
|
1394
1501
|
export function decodeClientTurnCommandV1(input: unknown): ClientTurnCommandV1 {
|
package/src/shared.ts
CHANGED
|
@@ -161,6 +161,60 @@ export interface PluginCatalogItem {
|
|
|
161
161
|
settings?: PackageSettingDefinition[];
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/** What an external authorization redirect told the app on the way back. */
|
|
165
|
+
export interface ConnectionReturnV1 {
|
|
166
|
+
/** The Package that owns the Connection, e.g. `composio`. */
|
|
167
|
+
packageId: string;
|
|
168
|
+
status: "ready" | "pending" | "failed";
|
|
169
|
+
/** A provider- or callback-supplied explanation, when there is one. */
|
|
170
|
+
reason?: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const CONNECTION_RETURN_PARAM = "connection";
|
|
174
|
+
const CONNECTION_RETURN_REASON_PARAM = "connection_reason";
|
|
175
|
+
const MAX_CONNECTION_RETURN_REASON = 300;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Read an authorization return out of a URL query string.
|
|
179
|
+
*
|
|
180
|
+
* The callback redirects to `/?connection=<packageId>-<status>`. Left unread it
|
|
181
|
+
* is a stale query string and nothing else: the User is returned to the app
|
|
182
|
+
* with no confirmation, and a `failed` grant vanishes entirely.
|
|
183
|
+
*/
|
|
184
|
+
export function decodeConnectionReturnV1(
|
|
185
|
+
search: string,
|
|
186
|
+
): ConnectionReturnV1 | undefined {
|
|
187
|
+
let params: URLSearchParams;
|
|
188
|
+
try {
|
|
189
|
+
params = new URLSearchParams(search);
|
|
190
|
+
} catch {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
const raw = params.get(CONNECTION_RETURN_PARAM);
|
|
194
|
+
if (!raw) return undefined;
|
|
195
|
+
const separator = raw.lastIndexOf("-");
|
|
196
|
+
if (separator <= 0) return undefined;
|
|
197
|
+
const packageId = raw.slice(0, separator);
|
|
198
|
+
const status = raw.slice(separator + 1);
|
|
199
|
+
if (status !== "ready" && status !== "pending" && status !== "failed") {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(packageId)) return undefined;
|
|
203
|
+
const reason = params
|
|
204
|
+
.get(CONNECTION_RETURN_REASON_PARAM)
|
|
205
|
+
?.slice(0, MAX_CONNECTION_RETURN_REASON);
|
|
206
|
+
return { packageId, status, ...(reason ? { reason } : {}) };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The same query string with the return parameters removed. */
|
|
210
|
+
export function withoutConnectionReturnV1(search: string): string {
|
|
211
|
+
const params = new URLSearchParams(search);
|
|
212
|
+
params.delete(CONNECTION_RETURN_PARAM);
|
|
213
|
+
params.delete(CONNECTION_RETURN_REASON_PARAM);
|
|
214
|
+
const rest = params.toString();
|
|
215
|
+
return rest ? `?${rest}` : "";
|
|
216
|
+
}
|
|
217
|
+
|
|
164
218
|
export interface FrockBotWebData {
|
|
165
219
|
connection: WebConnection;
|
|
166
220
|
modelLabel: string;
|
|
@@ -261,6 +315,12 @@ export interface FrockBotWebData {
|
|
|
261
315
|
*/
|
|
262
316
|
mcpServers?: McpServerStatusViewV1;
|
|
263
317
|
settingsError?: string;
|
|
318
|
+
/**
|
|
319
|
+
* What the browser came back from an external authorization with. Read once
|
|
320
|
+
* from the return URL at boot and cleared when the User has seen it, so a
|
|
321
|
+
* cancelled or failed grant is reported rather than silently discarded.
|
|
322
|
+
*/
|
|
323
|
+
connectionReturn?: ConnectionReturnV1;
|
|
264
324
|
selectBot(botId: string): Promise<void>;
|
|
265
325
|
loadBotSettings(): Promise<void>;
|
|
266
326
|
saveBotProfile(profile: BotProfile): Promise<void>;
|
|
@@ -365,6 +425,11 @@ export interface FrockBotWebData {
|
|
|
365
425
|
values?: Record<string, JsonValue>,
|
|
366
426
|
): Promise<void>;
|
|
367
427
|
uninstallPackage(packageId: string): Promise<void>;
|
|
428
|
+
/**
|
|
429
|
+
* Puts this conversation down and starts the next one. Memory is kept; only
|
|
430
|
+
* the history the next Turn carries is new (ADR 0027).
|
|
431
|
+
*/
|
|
432
|
+
startConversation(): Promise<void>;
|
|
368
433
|
startConnection(
|
|
369
434
|
packageId: string,
|
|
370
435
|
connectionTypeId: string,
|