@mercury-fw/channel-google-chat 0.7.0 → 0.8.0
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/CHANGELOG.md +14 -0
- package/dist/google-chat-provider.d.ts +3 -1
- package/google-chat-provider.ts +88 -37
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @mercury-fw/channel-google-chat
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b2f8cfb: - A confirmation can offer several choices (for example "only this time" / "always for this host"): a plugin tool stages them with the new `stageChoices` of `SessionToolContext`, and gets one token per choice. The person takes one; the others die with it, and they all expire together.
|
|
8
|
+
- The paper-trail note of a group of choices is one, under its first token, and says which choice was taken.
|
|
9
|
+
- `detectPendingConfirmation` returns the `choices` a staging offers; a plain staging is one choice labelled `Conferma`.
|
|
10
|
+
- A confirmed action can hand its result back to the model (`resume` on a choice): instead of showing the person the raw result, the channel resumes the turn and the model answers on it, the result reaching it as the tool result of the call that staged it, never stored in the history.
|
|
11
|
+
- A resumed turn answers the person who confirmed even in a shared space, and the line standing for the confirmation isn't archived as something they said. A result nobody resumes within five minutes (the life of a confirmation token) is forgotten, and a restart loses it.
|
|
12
|
+
- `InboundTurn.resumeAfterConfirm` and `RESUME_AFTER_CONFIRM` (channel contract) carry the resumed turn; `ConfirmOutcome` has a `resume` status.
|
|
13
|
+
- Terminal: every choice is printed on its own line with its token; a confirmation that resumes runs the turn again.
|
|
14
|
+
- Google Chat: the confirmation card has one button per choice; a typed token or a clicked choice that resumes runs the turn again, in the session's queue. The channel now needs channel-types 0.51.0 or later.
|
|
15
|
+
- HTTP: the `pending` event carries `choices`; a token sent to `POST /turn` that resumes streams the model's answer on the same request; `POST /confirm` answers `resume: true`, and `POST /turn { resume: true, conversationId }` runs that turn. The channel now needs channel-types 0.51.0 or later.
|
|
16
|
+
|
|
3
17
|
## 0.7.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
|
@@ -58,10 +58,12 @@ export type ParsedCardClickEvent = {
|
|
|
58
58
|
sender: string;
|
|
59
59
|
senderEmail: string | undefined;
|
|
60
60
|
parameters: Record<string, string>;
|
|
61
|
+
/** As on a message (see `ParsedMessageEvent.isDirectMessage`): for the turn a click resumes. */
|
|
62
|
+
isDirectMessage: boolean;
|
|
61
63
|
};
|
|
62
64
|
/** Parses one decoded Pub/Sub event, or `null` if it isn't a kind this provider acts on. */
|
|
63
65
|
export declare function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardClickEvent | null;
|
|
64
|
-
export type CardClickHandler = (params: Record<string, string>, space: string, sender: string, senderEmail?: string) => Promise<void>;
|
|
66
|
+
export type CardClickHandler = (params: Record<string, string>, space: string, sender: string, senderEmail?: string, isDirectMessage?: boolean) => Promise<void>;
|
|
65
67
|
export type GoogleChatProviderDeps = {
|
|
66
68
|
credentials: ServiceAccountCredentials;
|
|
67
69
|
subscription: string;
|
package/google-chat-provider.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
detectPendingConfirmation,
|
|
37
37
|
loginLinkText,
|
|
38
38
|
PENDING_CONFIRMATION_NOTE,
|
|
39
|
+
RESUME_AFTER_CONFIRM,
|
|
39
40
|
NO_REPLY,
|
|
40
41
|
type LoginRequest,
|
|
41
42
|
type PendingConfirmation,
|
|
@@ -48,7 +49,8 @@ import {
|
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
51
|
* Builds the confirmation card sent when a step stages an irreversible
|
|
51
|
-
* command (see `pending-confirmation.ts`)
|
|
52
|
+
* command (see `pending-confirmation.ts`), one button per choice it offers
|
|
53
|
+
* (a single `Conferma` for a plain staging). Each button's parameters carry
|
|
52
54
|
* the token, so a click routes straight into the same `tryConfirm` path
|
|
53
55
|
* a bare token typed on the terminal uses (see `onCardClick`'s `confirm`
|
|
54
56
|
* case, below) — the user never has to see or type the token themselves.
|
|
@@ -62,12 +64,10 @@ function buildConfirmCard(pending: PendingConfirmation): ChatCard {
|
|
|
62
64
|
{ textParagraph: { text: `\`${pending.summary}\`` } },
|
|
63
65
|
{
|
|
64
66
|
buttonList: {
|
|
65
|
-
buttons:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
},
|
|
70
|
-
],
|
|
67
|
+
buttons: pending.choices.map((choice) => ({
|
|
68
|
+
text: choice.label,
|
|
69
|
+
onClick: { action: { function: "confirm", parameters: [{ key: "token", value: choice.token }] } },
|
|
70
|
+
})),
|
|
71
71
|
},
|
|
72
72
|
},
|
|
73
73
|
],
|
|
@@ -196,7 +196,7 @@ type RawChatEvent = {
|
|
|
196
196
|
sender?: RawChatSender;
|
|
197
197
|
};
|
|
198
198
|
action?: { actionMethodName?: string; parameters?: Array<{ key?: string; value?: string }> };
|
|
199
|
-
space?: { name?: string };
|
|
199
|
+
space?: { name?: string; type?: string };
|
|
200
200
|
user?: RawChatSender;
|
|
201
201
|
};
|
|
202
202
|
|
|
@@ -225,6 +225,8 @@ export type ParsedCardClickEvent = {
|
|
|
225
225
|
sender: string;
|
|
226
226
|
senderEmail: string | undefined;
|
|
227
227
|
parameters: Record<string, string>;
|
|
228
|
+
/** As on a message (see `ParsedMessageEvent.isDirectMessage`): for the turn a click resumes. */
|
|
229
|
+
isDirectMessage: boolean;
|
|
228
230
|
};
|
|
229
231
|
|
|
230
232
|
/** The email Google vouches for on `sender`: a person's only (`HUMAN`), never a bot's or an unknown kind's. */
|
|
@@ -260,12 +262,25 @@ export function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardCli
|
|
|
260
262
|
parameters[p.key] = p.value;
|
|
261
263
|
}
|
|
262
264
|
}
|
|
263
|
-
return {
|
|
265
|
+
return {
|
|
266
|
+
kind: "card-click",
|
|
267
|
+
space: event.space.name,
|
|
268
|
+
sender: event.user.name,
|
|
269
|
+
senderEmail: emailOf(event.user),
|
|
270
|
+
parameters,
|
|
271
|
+
isDirectMessage: event.space.type === "DM",
|
|
272
|
+
};
|
|
264
273
|
}
|
|
265
274
|
return null;
|
|
266
275
|
}
|
|
267
276
|
|
|
268
|
-
export type CardClickHandler = (
|
|
277
|
+
export type CardClickHandler = (
|
|
278
|
+
params: Record<string, string>,
|
|
279
|
+
space: string,
|
|
280
|
+
sender: string,
|
|
281
|
+
senderEmail?: string,
|
|
282
|
+
isDirectMessage?: boolean,
|
|
283
|
+
) => Promise<void>;
|
|
269
284
|
|
|
270
285
|
export type GoogleChatProviderDeps = {
|
|
271
286
|
credentials: ServiceAccountCredentials;
|
|
@@ -319,7 +334,50 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
319
334
|
// concurrently. Scoped by sessionKey only (not global), so a slow turn
|
|
320
335
|
// for one user/space never blocks a different one's.
|
|
321
336
|
const busySessions = new Set<string>();
|
|
322
|
-
const
|
|
337
|
+
const queuedTurns = new Map<string, Array<() => Promise<void>>>();
|
|
338
|
+
// The turn handler `start` was given, for a turn a card click resumes.
|
|
339
|
+
let activeHandleTurn: HandleTurn | undefined;
|
|
340
|
+
|
|
341
|
+
/** Runs `turn` for `sessionKey` now, or after the turns already queued for it. */
|
|
342
|
+
async function runInSession(sessionKey: string, turn: () => Promise<void>): Promise<void> {
|
|
343
|
+
if (busySessions.has(sessionKey)) {
|
|
344
|
+
const queue = queuedTurns.get(sessionKey) ?? [];
|
|
345
|
+
queue.push(turn);
|
|
346
|
+
queuedTurns.set(sessionKey, queue);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
busySessions.add(sessionKey);
|
|
350
|
+
try {
|
|
351
|
+
await turn();
|
|
352
|
+
} finally {
|
|
353
|
+
busySessions.delete(sessionKey);
|
|
354
|
+
const queue = queuedTurns.get(sessionKey);
|
|
355
|
+
const next = queue?.shift();
|
|
356
|
+
if (queue && queue.length === 0) queuedTurns.delete(sessionKey);
|
|
357
|
+
if (next) {
|
|
358
|
+
runInSession(sessionKey, next).catch((err) => log(`[chat] queued turn failed: ${String(err)}`));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Resumes the session's turn after a confirmation whose result went back to the model (`RESUME_AFTER_CONFIRM`). */
|
|
364
|
+
function resumeTurn(handleTurn: HandleTurn, space: string, principal: Principal, isDirectMessage: boolean): Promise<void> {
|
|
365
|
+
const sessionKey = deriveSessionKey(space, principal.id);
|
|
366
|
+
return runInSession(sessionKey, () =>
|
|
367
|
+
handleTurn(
|
|
368
|
+
{
|
|
369
|
+
channel: "google-chat",
|
|
370
|
+
multiUser: !isDirectMessage,
|
|
371
|
+
text: "",
|
|
372
|
+
sessionKey,
|
|
373
|
+
principal,
|
|
374
|
+
logPrefix: `[chat:${space}:${principal.id}] `,
|
|
375
|
+
resumeAfterConfirm: true,
|
|
376
|
+
},
|
|
377
|
+
createSink(space, { user: principal.id, isDirectMessage }),
|
|
378
|
+
),
|
|
379
|
+
);
|
|
380
|
+
}
|
|
323
381
|
|
|
324
382
|
/**
|
|
325
383
|
* Default `onCardClick`: the confirm button's token routes through the exact
|
|
@@ -329,17 +387,22 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
329
387
|
*/
|
|
330
388
|
const onCardClick: CardClickHandler =
|
|
331
389
|
deps.onCardClick ??
|
|
332
|
-
(async (params, space, sender, senderEmail) => {
|
|
390
|
+
(async (params, space, sender, senderEmail, isDirectMessage = false) => {
|
|
333
391
|
const token = params.token;
|
|
334
392
|
if (!token) {
|
|
335
393
|
log(`[chat] card click with no token parameter`);
|
|
336
394
|
return;
|
|
337
395
|
}
|
|
338
|
-
const
|
|
396
|
+
const principal: Principal = {
|
|
339
397
|
id: sender,
|
|
340
398
|
provider: "google-chat",
|
|
341
399
|
...(senderEmail === undefined ? {} : { claims: { email: senderEmail } }),
|
|
342
|
-
}
|
|
400
|
+
};
|
|
401
|
+
const reply = await deps.confirm(token, deriveSessionKey(space, sender), principal);
|
|
402
|
+
if (reply === RESUME_AFTER_CONFIRM && activeHandleTurn) {
|
|
403
|
+
await resumeTurn(activeHandleTurn, space, principal, isDirectMessage);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
343
406
|
if (reply !== null) {
|
|
344
407
|
log(`[chat:${space}] [out] ${reply}`);
|
|
345
408
|
const sent = await sendMessageFn(space, reply, clientDeps);
|
|
@@ -528,6 +591,10 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
528
591
|
};
|
|
529
592
|
|
|
530
593
|
const confirmReply = await deps.confirm(event.text, sessionKey, principal);
|
|
594
|
+
if (confirmReply === RESUME_AFTER_CONFIRM) {
|
|
595
|
+
await resumeTurn(handleTurn, event.space, principal, event.isDirectMessage);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
531
598
|
if (confirmReply !== null) {
|
|
532
599
|
log(`[chat:${event.space}] [out] ${confirmReply}`);
|
|
533
600
|
const sent = await sendMessageFn(event.space, confirmReply, clientDeps);
|
|
@@ -538,17 +605,8 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
538
605
|
// Only the actual model turn needs serializing — tryConfirm above
|
|
539
606
|
// never touches SessionHistory, so it's always safe to run right
|
|
540
607
|
// away regardless of whether this session is mid-turn.
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
queue.push(event);
|
|
544
|
-
queuedEvents.set(sessionKey, queue);
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
busySessions.add(sessionKey);
|
|
549
|
-
try {
|
|
550
|
-
const sink = createSink(event.space, { user: event.sender, isDirectMessage: event.isDirectMessage });
|
|
551
|
-
await handleTurn(
|
|
608
|
+
await runInSession(sessionKey, () =>
|
|
609
|
+
handleTurn(
|
|
552
610
|
{
|
|
553
611
|
channel: "google-chat",
|
|
554
612
|
multiUser: !event.isDirectMessage,
|
|
@@ -557,17 +615,9 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
557
615
|
principal,
|
|
558
616
|
logPrefix: `[chat:${event.space}:${event.sender}] `,
|
|
559
617
|
},
|
|
560
|
-
|
|
561
|
-
)
|
|
562
|
-
|
|
563
|
-
busySessions.delete(sessionKey);
|
|
564
|
-
const queue = queuedEvents.get(sessionKey);
|
|
565
|
-
const next = queue?.shift();
|
|
566
|
-
if (queue && queue.length === 0) queuedEvents.delete(sessionKey);
|
|
567
|
-
if (next) {
|
|
568
|
-
processMessageEvent(next, handleTurn).catch((err) => log(`[chat] queued event handling failed: ${String(err)}`));
|
|
569
|
-
}
|
|
570
|
-
}
|
|
618
|
+
createSink(event.space, { user: event.sender, isDirectMessage: event.isDirectMessage }),
|
|
619
|
+
),
|
|
620
|
+
);
|
|
571
621
|
}
|
|
572
622
|
|
|
573
623
|
/**
|
|
@@ -598,7 +648,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
598
648
|
if (parsed.kind === "message") {
|
|
599
649
|
await processMessageEvent(parsed, handleTurn);
|
|
600
650
|
} else {
|
|
601
|
-
await onCardClick(parsed.parameters, parsed.space, parsed.sender, parsed.senderEmail);
|
|
651
|
+
await onCardClick(parsed.parameters, parsed.space, parsed.sender, parsed.senderEmail, parsed.isDirectMessage);
|
|
602
652
|
}
|
|
603
653
|
} catch (err) {
|
|
604
654
|
log(`[chat] event handling failed: ${String(err)}`);
|
|
@@ -607,6 +657,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
|
|
|
607
657
|
|
|
608
658
|
return {
|
|
609
659
|
async start(handleTurn: HandleTurn): Promise<void> {
|
|
660
|
+
activeHandleTurn = handleTurn;
|
|
610
661
|
const sub = subscriptionFn(deps.credentials, deps.subscription);
|
|
611
662
|
activeSubscription = sub;
|
|
612
663
|
sub.on("message", (message) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mercury-fw/channel-google-chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"@google-cloud/pubsub": "^5.3.1"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@mercury-fw/channel-types": ">=0.
|
|
34
|
+
"@mercury-fw/channel-types": ">=0.51.0 <1.0.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@mercury-fw/channel-types": "0.
|
|
37
|
+
"@mercury-fw/channel-types": "0.51.0",
|
|
38
38
|
"@mercury-fw/typescript-config": "*",
|
|
39
39
|
"@types/bun": "^1.4.2",
|
|
40
40
|
"typescript": "^6.0.3"
|