@coffer-org/plugin-webchat 3.1.1 → 5.0.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/dist/index.js CHANGED
@@ -4,19 +4,20 @@ import { field } from '@coffer-org/sdk/fields';
4
4
  export default definePlugin({
5
5
  id: 'webchat',
6
6
  version: '1.0.0',
7
- dependsOn: ['orchestrator'],
7
+ dependsOn: [],
8
8
  settings: defineSettings({
9
9
  label: 'webchat.settings.label',
10
- fields: [
11
- field.select({
12
- key: 'reasoning_display',
10
+ fields: {
11
+ reasoning_display: field.string({
13
12
  label: 'webchat.settings.reasoning_display',
14
13
  default: 'live',
14
+ strict: true,
15
+ noSearch: true,
15
16
  options: [
16
17
  { value: 'live', title: 'webchat.settings.reasoning_display_live' },
17
18
  { value: 'off', title: 'webchat.settings.reasoning_display_off' },
18
19
  ],
19
20
  }),
20
- ],
21
+ },
21
22
  }),
22
23
  });
@@ -1,5 +1,5 @@
1
1
  import { type ActionCaller } from '@coffer-org/server/plugin-hooks';
2
- import { type AgentDescriptor } from '@coffer-org/plugin-orchestrator/runtime';
2
+ import { type AgentDescriptor } from '@coffer-org/server/orchestrator';
3
3
  import { type ThreadSelection } from './chain-store.ts';
4
4
  export interface ThreadSummary {
5
5
  convId: string;
@@ -13,6 +13,7 @@ export interface HistoryMsg {
13
13
  text: string;
14
14
  ts: number;
15
15
  reasoning?: string;
16
+ suggestions?: string[];
16
17
  attachments?: {
17
18
  name: string;
18
19
  mime?: string;
@@ -32,6 +33,7 @@ export declare function agentsAction(body: Record<string, unknown>, caller: Acti
32
33
  agents: AgentDescriptor[];
33
34
  defaultAgentId: string | null;
34
35
  selection: ThreadSelection;
36
+ starters: string[];
35
37
  }>;
36
38
  export declare function selectAgentAction(body: Record<string, unknown>, caller: ActionCaller): Promise<{
37
39
  selection: ThreadSelection;
@@ -1,7 +1,16 @@
1
1
  import { HttpError } from '@coffer-org/server/plugin-hooks';
2
- import { listAgentCatalog, getDefaultAgentId, listRegisteredAgents, } from '@coffer-org/plugin-orchestrator/runtime';
2
+ import { listAgentCatalog, getDefaultAgentId, listRegisteredAgents, getConversationStarters, } from '@coffer-org/server/orchestrator';
3
3
  import { chatIdFor, conversations, history, getSelection, setSelection } from "./chain-store.js";
4
4
  const TITLE_MAX = 60;
5
+ function parseSuggestions(text) {
6
+ try {
7
+ const parsed = JSON.parse(text);
8
+ return Array.isArray(parsed) && parsed.every((v) => typeof v === 'string') ? parsed : null;
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
5
14
  function title(firstUserText) {
6
15
  const t = firstUserText.trim().replace(/\s+/g, ' ');
7
16
  return t.length <= TITLE_MAX ? t : `${t.slice(0, TITLE_MAX - 1)}…`;
@@ -28,7 +37,11 @@ export async function historyAction(body, caller) {
28
37
  const convId = requireConvId(body);
29
38
  const rows = await history(chatIdFor(caller.id, convId));
30
39
  const reasoningByBot = new Map(rows.filter((m) => m.role === 'reasoning').map((m) => [m.msgId.slice(0, -2), m.text]));
31
- const visible = rows.filter((m) => m.role !== 'reasoning');
40
+ const suggestionsByBot = new Map(rows
41
+ .filter((m) => m.role === 'suggestions')
42
+ .map((m) => [m.msgId.slice(0, -2), parseSuggestions(m.text)])
43
+ .filter((entry) => entry[1] !== null));
44
+ const visible = rows.filter((m) => m.role !== 'reasoning' && m.role !== 'suggestions');
32
45
  return {
33
46
  convId,
34
47
  messages: visible.map((m) => ({
@@ -37,6 +50,7 @@ export async function historyAction(body, caller) {
37
50
  text: m.text,
38
51
  ts: m.ts,
39
52
  ...(reasoningByBot.has(m.msgId) ? { reasoning: reasoningByBot.get(m.msgId) } : {}),
53
+ ...(suggestionsByBot.has(m.msgId) ? { suggestions: suggestionsByBot.get(m.msgId) } : {}),
40
54
  ...(m.attachments?.length ? { attachments: m.attachments } : {}),
41
55
  })),
42
56
  headMsgId: visible.at(-1)?.msgId ?? null,
@@ -48,6 +62,7 @@ export async function agentsAction(body, caller) {
48
62
  agents: await listAgentCatalog(),
49
63
  defaultAgentId: getDefaultAgentId() ?? null,
50
64
  selection: await getSelection(chatIdFor(caller.id, convId)),
65
+ starters: await getConversationStarters(),
51
66
  };
52
67
  }
53
68
  export async function selectAgentAction(body, caller) {
@@ -1,6 +1,6 @@
1
1
  import { type ThreadChat, type StoredMsg } from '@coffer-org/server/thread-store';
2
2
  import { type ThreadSelection } from '@coffer-org/server/thread-state';
3
- import type { ConvMessage } from '@coffer-org/plugin-orchestrator/runtime';
3
+ import type { ConvMessage } from '@coffer-org/server/orchestrator';
4
4
  export declare const CONNECTOR = "webchat";
5
5
  export declare function chatIdFor(userId: string, convId: string): string;
6
6
  export declare function chatPrefixFor(userId: string): string;
@@ -27,6 +27,13 @@ export declare function recordReasoning(m: {
27
27
  text: string;
28
28
  now: number;
29
29
  }): Promise<boolean>;
30
+ export declare function recordSuggestions(m: {
31
+ chatId: string;
32
+ botMsgId: string;
33
+ parentMsgId: string | null;
34
+ suggestions: string[];
35
+ now: number;
36
+ }): Promise<boolean>;
30
37
  export declare function buildChain(headMsgId: string, opts: {
31
38
  chatId: string;
32
39
  maxDepth?: number;
@@ -51,6 +51,20 @@ export async function recordReasoning(m) {
51
51
  });
52
52
  return true;
53
53
  }
54
+ export async function recordSuggestions(m) {
55
+ if (!m.suggestions.length)
56
+ return false;
57
+ await putThreadMessage({
58
+ connector: CONNECTOR,
59
+ chatId: m.chatId,
60
+ msgId: `${m.botMsgId}~s`,
61
+ role: 'suggestions',
62
+ text: JSON.stringify(m.suggestions),
63
+ ts: m.now - 1,
64
+ replyToId: m.parentMsgId,
65
+ });
66
+ return true;
67
+ }
54
68
  export async function buildChain(headMsgId, opts) {
55
69
  const max = opts.maxDepth ?? DEFAULT_MAX_DEPTH;
56
70
  const acc = [];
@@ -61,7 +75,7 @@ export async function buildChain(headMsgId, opts) {
61
75
  const stored = await getThreadMessage(CONNECTOR, opts.chatId, cur);
62
76
  if (!stored)
63
77
  break;
64
- if (stored.role !== 'reasoning') {
78
+ if (stored.role !== 'reasoning' && stored.role !== 'suggestions') {
65
79
  acc.push({
66
80
  role: stored.role,
67
81
  content: stored.text,
@@ -1,2 +1,2 @@
1
- import type { GatePolicy } from '@coffer-org/plugin-orchestrator/runtime';
1
+ import type { GatePolicy } from '@coffer-org/server/orchestrator';
2
2
  export declare function policy(): GatePolicy;
@@ -1,9 +1,10 @@
1
- import type { Connector } from '@coffer-org/plugin-orchestrator/runtime';
2
- import { recordAssistant as storeAssistant, recordReasoning as storeReasoning } from './chain-store.ts';
1
+ import type { Connector } from '@coffer-org/server/orchestrator';
2
+ import { recordAssistant as storeAssistant, recordReasoning as storeReasoning, recordSuggestions as storeSuggestions } from './chain-store.ts';
3
3
  import type { ReasoningDisplay } from './settings.ts';
4
4
  export interface StreamingConnector {
5
5
  connector: Connector;
6
6
  recorded(): string | null;
7
+ suggestions(): string[] | null;
7
8
  }
8
9
  export interface StreamingConnectorOpts {
9
10
  chatId: string;
@@ -12,5 +13,6 @@ export interface StreamingConnectorOpts {
12
13
  display: ReasoningDisplay;
13
14
  record?: typeof storeAssistant;
14
15
  recordReasoning?: typeof storeReasoning;
16
+ recordSuggestions?: typeof storeSuggestions;
15
17
  }
16
18
  export declare function makeStreamingConnector(opts: StreamingConnectorOpts): StreamingConnector;
@@ -1,11 +1,13 @@
1
- import { makeLiveChannel, plainRender } from '@coffer-org/plugin-orchestrator/runtime';
2
- import { recordAssistant as storeAssistant, recordReasoning as storeReasoning } from "./chain-store.js";
1
+ import { makeLiveChannel, plainRender } from '@coffer-org/server/orchestrator';
2
+ import { recordAssistant as storeAssistant, recordReasoning as storeReasoning, recordSuggestions as storeSuggestions, } from "./chain-store.js";
3
3
  const WEBCHAT_THROTTLE_MS = 250;
4
4
  const WEBCHAT_MAX = 100_000;
5
5
  export function makeStreamingConnector(opts) {
6
6
  const record = opts.record ?? storeAssistant;
7
7
  const recordReasoningFn = opts.recordReasoning ?? storeReasoning;
8
+ const recordSuggestionsFn = opts.recordSuggestions ?? storeSuggestions;
8
9
  let recordedId = null;
10
+ let lastSuggestions = null;
9
11
  const connector = {
10
12
  id: 'webchat',
11
13
  reply(_chatId, _ctx) {
@@ -25,6 +27,10 @@ export function makeStreamingConnector(opts) {
25
27
  });
26
28
  return {
27
29
  ...ch,
30
+ async finish(r) {
31
+ lastSuggestions = r.suggestions;
32
+ return ch.finish(r);
33
+ },
28
34
  updateReasoning(acc) {
29
35
  if (opts.display === 'off')
30
36
  return;
@@ -51,10 +57,20 @@ export function makeStreamingConnector(opts) {
51
57
  now,
52
58
  });
53
59
  }
60
+ if (wrote && lastSuggestions?.length && m.botMsgId) {
61
+ await recordSuggestionsFn({
62
+ chatId: opts.chatId,
63
+ botMsgId: m.botMsgId,
64
+ parentMsgId: m.parentMsgId,
65
+ suggestions: lastSuggestions,
66
+ now,
67
+ });
68
+ }
54
69
  },
55
70
  };
56
71
  return {
57
72
  connector,
58
73
  recorded: () => recordedId,
74
+ suggestions: () => lastSuggestions,
59
75
  };
60
76
  }
@@ -1 +1,2 @@
1
1
  export declare const WEB_FORMAT: string;
2
+ export declare function webChannelSystem(): Promise<string>;
@@ -1,3 +1,4 @@
1
+ import { frontendInstructions } from '@coffer-org/server/frontend-agent';
1
2
  export const WEB_FORMAT = [
2
3
  'CRITICAL OUTPUT FORMAT — MARKDOWN ONLY. Format the reply strictly as Markdown (GFM). NEVER emit raw HTML tags — they are stripped before rendering and their content may be lost.',
3
4
  'Inline: **bold**, *italic*, `inline code`, ~~strikethrough~~. Links: [text](https://…).',
@@ -8,3 +9,7 @@ export const WEB_FORMAT = [
8
9
  'The reply is read in a narrow chat panel (~360px) — keep it short and skimmable: short paragraphs, lists over prose, narrow tables.',
9
10
  'Use Unicode symbols and emoji to structure replies: arrows (→), status marks (✅ ⚠️ ❌ ⏳), topic emoji (📅 💰 🏥 🛒 🌱 …).',
10
11
  ].join('\n');
12
+ export async function webChannelSystem() {
13
+ const site = await frontendInstructions('');
14
+ return [WEB_FORMAT, site].filter(Boolean).join('\n\n');
15
+ }
@@ -3,7 +3,7 @@ import { pruneThreadState } from '@coffer-org/server/thread-state';
3
3
  import { threadsAction, historyAction, agentsAction, selectAgentAction } from "./actions.js";
4
4
  import { sendAction } from "./send.js";
5
5
  import { CONNECTOR } from "./chain-store.js";
6
- import { registerConnector } from '@coffer-org/plugin-orchestrator/runtime';
6
+ import { registerConnector } from '@coffer-org/server/orchestrator';
7
7
  const THREAD_TTL_MS = Number(process.env['WEBCHAT_THREAD_TTL_MS'] ?? 30 * 86_400_000);
8
8
  let unregisterConnector;
9
9
  export { WEB_FORMAT } from "./format.js";
@@ -1,4 +1,4 @@
1
- import { handleIncoming } from '@coffer-org/plugin-orchestrator/runtime';
1
+ import { handleIncoming } from '@coffer-org/server/orchestrator';
2
2
  import type { ActionCaller } from '@coffer-org/server/plugin-hooks';
3
3
  export interface SendDeps {
4
4
  handleIncoming?: typeof handleIncoming;
@@ -1,9 +1,9 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { handleIncoming, liveAgentId } from '@coffer-org/plugin-orchestrator/runtime';
2
+ import { handleIncoming, liveAgentId } from '@coffer-org/server/orchestrator';
3
3
  import { chatIdFor, recordUser, buildChain, history, readSelectionForTurn } from "./chain-store.js";
4
4
  import { policy } from "./config.js";
5
5
  import { makeStreamingConnector } from "./connector.js";
6
- import { WEB_FORMAT } from "./format.js";
6
+ import { webChannelSystem } from "./format.js";
7
7
  import { loadReasoningDisplay } from "./settings.js";
8
8
  export function pageContext(ctx) {
9
9
  if (typeof ctx !== 'object' || ctx === null)
@@ -41,24 +41,25 @@ export async function sendAction(body, ctx, deps = {}) {
41
41
  const replyTo = explicitReplyTo ?? (await history(chatId, 1)).at(-1)?.msgId ?? null;
42
42
  await recordUser({ chatId, msgId, sender: ctx.caller.id, text, attachments, ts: nowSec, replyToId: replyTo });
43
43
  const messages = await buildChain(msgId, { chatId });
44
- const { connector, recorded } = makeStreamingConnector({
44
+ const { connector, recorded, suggestions } = makeStreamingConnector({
45
45
  chatId,
46
46
  botMsgId,
47
47
  emit: ctx.emit,
48
48
  display: await loadReasoningDisplay(),
49
49
  });
50
- const volatileSystem = pageContext(body['context']);
50
+ const turnContext = pageContext(body['context']);
51
51
  await doHandle(connector, {
52
52
  connectorId: 'webchat',
53
- channelSystem: WEB_FORMAT,
54
- ...(volatileSystem ? { volatileSystem } : {}),
53
+ channelSystem: await webChannelSystem(),
54
+ ...(turnContext ? { turnContext } : {}),
55
55
  ...(agentId ? { agentId } : {}),
56
56
  ...(selection.presetId ? { presetId: selection.presetId } : {}),
57
57
  chatId,
58
58
  sender: { id: ctx.caller.id },
59
59
  messages,
60
+ supportsSuggestions: true,
60
61
  }, { policy: policy() });
61
- ctx.emit('done', { msgId: recorded(), parentMsgId: msgId });
62
+ ctx.emit('done', { msgId: recorded(), parentMsgId: msgId, suggestions: suggestions() });
62
63
  }
63
64
  function parseAttachments(value) {
64
65
  if (!Array.isArray(value))