@coffer-org/plugin-webchat 7.0.0 → 7.0.1

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.
@@ -1,11 +1,19 @@
1
1
  import { type ActionCaller } from '@coffer-org/server/plugin-hooks';
2
- import { type AgentDescriptor } from '@coffer-org/server/orchestrator';
2
+ import { type AgentDescriptor, type ConnectorCapabilities } from '@coffer-org/server/orchestrator';
3
3
  import { type ThreadSelection } from './chain-store.ts';
4
+ export interface StreamCtx {
5
+ caller: ActionCaller;
6
+ emit: (event: string, data: unknown) => void;
7
+ signal: AbortSignal;
8
+ }
4
9
  export interface ThreadSummary {
5
10
  convId: string;
6
11
  title: string;
7
12
  lastTs: number;
8
13
  count: number;
14
+ ownerName: string | null;
15
+ mine: boolean;
16
+ private: boolean;
9
17
  }
10
18
  export interface HistoryMsg {
11
19
  msgId: string;
@@ -38,3 +46,11 @@ export declare function agentsAction(body: Record<string, unknown>, caller: Acti
38
46
  export declare function selectAgentAction(body: Record<string, unknown>, caller: ActionCaller): Promise<{
39
47
  selection: ThreadSelection;
40
48
  }>;
49
+ export declare function channelAction(body: Record<string, unknown>, ctx: StreamCtx): Promise<void>;
50
+ export interface SetVisibilityDeps {
51
+ capabilities?: ConnectorCapabilities;
52
+ }
53
+ export declare function setVisibilityAction(body: Record<string, unknown>, caller: ActionCaller, deps?: SetVisibilityDeps): Promise<{
54
+ visibility: 'private' | null;
55
+ }>;
56
+ export declare function stopAction(body: Record<string, unknown>, caller: ActionCaller): Promise<void>;
@@ -1,6 +1,11 @@
1
1
  import { HttpError } from '@coffer-org/server/plugin-hooks';
2
- import { listAgentCatalog, getDefaultAgentId, listRegisteredAgents, getConversationStarters, } from '@coffer-org/server/orchestrator';
3
- import { chatIdFor, conversations, history, getSelection, setSelection } from "./chain-store.js";
2
+ import { SIDECAR_ROLES } from '@coffer-org/server/thread-store';
3
+ import { listUsers } from '@coffer-org/server/auth-store';
4
+ import { abortTurn } from '@coffer-org/server/turn-gate';
5
+ import { listAgentCatalog, getDefaultAgentId, listRegisteredAgents, getConversationStarters, mayBePrivate, mayRead, } from '@coffer-org/server/orchestrator';
6
+ import { openConversation, conversations, history, getSelection, setSelection, } from "./chain-store.js";
7
+ import { CAPABILITIES } from "./send.js";
8
+ import { subscribe, evictWhere } from "./channel-registry.js";
4
9
  const TITLE_MAX = 60;
5
10
  function parseSuggestions(text) {
6
11
  try {
@@ -17,13 +22,17 @@ function title(firstUserText) {
17
22
  }
18
23
  export async function threadsAction(_body, caller) {
19
24
  const chats = await conversations(caller.id);
20
- const prefixLen = `u:${caller.id}:`.length;
25
+ const users = await listUsers();
26
+ const nameById = new Map(users.map((u) => [String(u.id), u.displayName]));
21
27
  return {
22
28
  threads: chats.map((c) => ({
23
- convId: c.chatId.slice(prefixLen),
24
- title: title(c.firstUserText),
29
+ convId: c.convId,
30
+ title: c.title ?? title(c.firstUserText),
25
31
  lastTs: c.lastTs,
26
32
  count: c.count,
33
+ ownerName: c.owner === null ? null : (nameById.get(c.owner) ?? null),
34
+ mine: c.owner === caller.id,
35
+ private: c.visibility === 'private',
27
36
  })),
28
37
  };
29
38
  }
@@ -33,15 +42,19 @@ function requireConvId(body) {
33
42
  throw new HttpError(400, 'missing required field: convId');
34
43
  return convId;
35
44
  }
45
+ const EMPTY_SELECTION = { agentId: null, presetId: null, title: null, owner: null, visibility: null };
36
46
  export async function historyAction(body, caller) {
37
47
  const convId = requireConvId(body);
38
- const rows = await history(chatIdFor(caller.id, convId));
48
+ const chatId = await openConversation(convId, caller.id, 'read');
49
+ if (chatId === null)
50
+ return { convId, messages: [], headMsgId: null };
51
+ const rows = await history(chatId);
39
52
  const reasoningByBot = new Map(rows.filter((m) => m.role === 'reasoning').map((m) => [m.msgId.slice(0, -2), m.text]));
40
53
  const suggestionsByBot = new Map(rows
41
54
  .filter((m) => m.role === 'suggestions')
42
55
  .map((m) => [m.msgId.slice(0, -2), parseSuggestions(m.text)])
43
56
  .filter((entry) => entry[1] !== null));
44
- const visible = rows.filter((m) => m.role !== 'reasoning' && m.role !== 'suggestions');
57
+ const visible = rows.filter((m) => !SIDECAR_ROLES.includes(m.role));
45
58
  return {
46
59
  convId,
47
60
  messages: visible.map((m) => ({
@@ -58,16 +71,19 @@ export async function historyAction(body, caller) {
58
71
  }
59
72
  export async function agentsAction(body, caller) {
60
73
  const convId = requireConvId(body);
74
+ const chatId = await openConversation(convId, caller.id, 'read');
61
75
  return {
62
76
  agents: await listAgentCatalog(),
63
77
  defaultAgentId: getDefaultAgentId() ?? null,
64
- selection: await getSelection(chatIdFor(caller.id, convId)),
78
+ selection: chatId === null ? EMPTY_SELECTION : await getSelection(chatId),
65
79
  starters: await getConversationStarters(),
66
80
  };
67
81
  }
68
82
  export async function selectAgentAction(body, caller) {
69
83
  const convId = requireConvId(body);
70
- const chatId = chatIdFor(caller.id, convId);
84
+ const chatId = await openConversation(convId, caller.id, 'write');
85
+ if (chatId === null)
86
+ return { selection: EMPTY_SELECTION };
71
87
  const patch = {};
72
88
  if ('agentId' in body) {
73
89
  const agentId = typeof body['agentId'] === 'string' && body['agentId'] ? body['agentId'] : null;
@@ -80,3 +96,50 @@ export async function selectAgentAction(body, caller) {
80
96
  await setSelection(chatId, patch);
81
97
  return { selection: await getSelection(chatId) };
82
98
  }
99
+ export async function channelAction(body, ctx) {
100
+ const convId = requireConvId(body);
101
+ const chatId = await openConversation(convId, ctx.caller.id, 'read');
102
+ if (chatId === null)
103
+ return;
104
+ let resolveHeld;
105
+ const held = new Promise((resolve) => {
106
+ resolveHeld = resolve;
107
+ });
108
+ const off = subscribe(chatId, {
109
+ viewerId: ctx.caller.id,
110
+ emit: ctx.emit,
111
+ close: () => resolveHeld(),
112
+ });
113
+ try {
114
+ if (!ctx.signal.aborted) {
115
+ ctx.signal.addEventListener('abort', () => resolveHeld(), { once: true });
116
+ await held;
117
+ }
118
+ }
119
+ finally {
120
+ off();
121
+ }
122
+ }
123
+ export async function setVisibilityAction(body, caller, deps = {}) {
124
+ const convId = requireConvId(body);
125
+ const makePrivate = body['private'] === true;
126
+ const chatId = await openConversation(convId, caller.id, 'write');
127
+ if (chatId === null)
128
+ return { visibility: null };
129
+ const selection = await getSelection(chatId);
130
+ if (selection.owner !== caller.id)
131
+ return { visibility: selection.visibility };
132
+ if (makePrivate && !mayBePrivate(deps.capabilities ?? CAPABILITIES))
133
+ return { visibility: selection.visibility };
134
+ const visibility = makePrivate ? 'private' : null;
135
+ await setSelection(chatId, { visibility });
136
+ evictWhere(chatId, (viewerId) => !mayRead({ owner: selection.owner, visibility }, viewerId));
137
+ return { visibility };
138
+ }
139
+ export async function stopAction(body, caller) {
140
+ const convId = requireConvId(body);
141
+ const chatId = await openConversation(convId, caller.id, 'write');
142
+ if (chatId === null)
143
+ return;
144
+ abortTurn('webchat', chatId);
145
+ }
@@ -1,9 +1,8 @@
1
- import { type ThreadChat, type StoredMsg } from '@coffer-org/server/thread-store';
1
+ import { 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/server/orchestrator';
3
+ import type { ConvMessage, ContextFact } from '@coffer-org/server/orchestrator';
4
4
  export declare const CONNECTOR = "webchat";
5
5
  export declare function chatIdFor(userId: string, convId: string): string;
6
- export declare function chatPrefixFor(userId: string): string;
7
6
  export declare function recordUser(m: {
8
7
  chatId: string;
9
8
  msgId: string;
@@ -34,13 +33,30 @@ export declare function recordSuggestions(m: {
34
33
  suggestions: string[];
35
34
  now: number;
36
35
  }): Promise<boolean>;
36
+ export declare function recordContext(m: {
37
+ chatId: string;
38
+ userMsgId: string;
39
+ facts: ContextFact[];
40
+ ts: number;
41
+ }): Promise<void>;
42
+ export declare function userTurnCount(chatId: string): Promise<number>;
37
43
  export declare function buildChain(headMsgId: string, opts: {
38
44
  chatId: string;
39
45
  maxDepth?: number;
40
46
  }): Promise<ConvMessage[]>;
41
47
  export declare function history(chatId: string, limit?: number): Promise<StoredMsg[]>;
42
- export declare function conversations(userId: string): Promise<ThreadChat[]>;
48
+ export interface ConversationSummary {
49
+ convId: string;
50
+ lastTs: number;
51
+ count: number;
52
+ firstUserText: string;
53
+ title: string | null;
54
+ owner: string | null;
55
+ visibility: 'private' | null;
56
+ }
57
+ export declare function conversations(viewerId: string): Promise<ConversationSummary[]>;
43
58
  export type { ThreadSelection };
59
+ export declare function openConversation(convId: string, viewerId: string, need: 'read' | 'write'): Promise<string | null>;
44
60
  export declare function getSelection(chatId: string): Promise<ThreadSelection>;
45
61
  export declare function setSelection(chatId: string, patch: Partial<ThreadSelection>): Promise<void>;
46
62
  export declare function readSelectionForTurn(chatId: string): Promise<ThreadSelection>;
@@ -1,5 +1,6 @@
1
- import { getThreadMessage, putThreadMessage, listThreadMessages, listThreadChats, } from '@coffer-org/server/thread-store';
2
- import { getThreadState, setThreadState, readAndTouchThreadState, } from '@coffer-org/server/thread-state';
1
+ import { getThreadMessage, putThreadMessage, listThreadMessages, listAllThreadChats, countUserTurns, SIDECAR_ROLES, } from '@coffer-org/server/thread-store';
2
+ import { getThreadState, getThreadStates, setThreadState, readAndTouchThreadState, findChatByConvId, } from '@coffer-org/server/thread-state';
3
+ import { CONNECTOR_FACT_NAME, mayRead, mayWrite } from '@coffer-org/server/orchestrator';
3
4
  import { getLogger } from '@coffer-org/sdk/logger';
4
5
  const log = getLogger('webchat');
5
6
  export const CONNECTOR = 'webchat';
@@ -7,8 +8,10 @@ const DEFAULT_MAX_DEPTH = 40;
7
8
  export function chatIdFor(userId, convId) {
8
9
  return `u:${userId}:${convId}`;
9
10
  }
10
- export function chatPrefixFor(userId) {
11
- return `u:${userId}:`;
11
+ function convIdOf(chatId) {
12
+ const first = chatId.indexOf(':');
13
+ const second = first === -1 ? -1 : chatId.indexOf(':', first + 1);
14
+ return second === -1 ? null : chatId.slice(second + 1);
12
15
  }
13
16
  export async function recordUser(m) {
14
17
  await putThreadMessage({
@@ -65,17 +68,46 @@ export async function recordSuggestions(m) {
65
68
  });
66
69
  return true;
67
70
  }
71
+ export async function recordContext(m) {
72
+ await putThreadMessage({
73
+ connector: CONNECTOR,
74
+ chatId: m.chatId,
75
+ msgId: `${m.userMsgId}~c`,
76
+ role: 'context',
77
+ text: JSON.stringify(m.facts),
78
+ ts: m.ts,
79
+ replyToId: null,
80
+ });
81
+ }
82
+ function parseFacts(text) {
83
+ try {
84
+ const parsed = JSON.parse(text);
85
+ return Array.isArray(parsed)
86
+ ? parsed.filter((f) => typeof f?.name === 'string' &&
87
+ CONNECTOR_FACT_NAME.test(f.name) &&
88
+ typeof f.value === 'string' &&
89
+ Object.entries(f.attrs ?? {}).every(([k, v]) => CONNECTOR_FACT_NAME.test(k) && typeof v === 'string'))
90
+ : [];
91
+ }
92
+ catch {
93
+ return [];
94
+ }
95
+ }
96
+ export function userTurnCount(chatId) {
97
+ return countUserTurns(CONNECTOR, chatId);
98
+ }
68
99
  export async function buildChain(headMsgId, opts) {
69
100
  const max = opts.maxDepth ?? DEFAULT_MAX_DEPTH;
70
101
  const acc = [];
102
+ let turns = 0;
71
103
  let cur = headMsgId;
72
104
  const seen = new Set();
73
- while (cur && acc.length < max && !seen.has(cur)) {
105
+ while (cur && turns < max && !seen.has(cur)) {
74
106
  seen.add(cur);
75
107
  const stored = await getThreadMessage(CONNECTOR, opts.chatId, cur);
76
108
  if (!stored)
77
109
  break;
78
- if (stored.role !== 'reasoning' && stored.role !== 'suggestions') {
110
+ if (!SIDECAR_ROLES.includes(stored.role)) {
79
111
  acc.push({
80
112
  role: stored.role,
81
113
  content: stored.text,
@@ -84,10 +116,17 @@ export async function buildChain(headMsgId, opts) {
84
116
  msgId: stored.msgId,
85
117
  ts: stored.ts,
86
118
  });
119
+ turns += 1;
120
+ if (stored.role === 'user') {
121
+ const ctx = await getThreadMessage(CONNECTOR, opts.chatId, `${stored.msgId}~c`);
122
+ const facts = ctx ? parseFacts(ctx.text) : [];
123
+ if (facts.length)
124
+ acc[acc.length - 1].context = facts;
125
+ }
87
126
  }
88
127
  cur = stored.replyToId;
89
128
  }
90
- if (acc.length >= max)
129
+ if (turns >= max)
91
130
  log.info(`chain capped at ${max} for ${headMsgId}`);
92
131
  while (acc.length && acc.at(-1)?.role !== 'user')
93
132
  acc.pop();
@@ -96,8 +135,46 @@ export async function buildChain(headMsgId, opts) {
96
135
  export function history(chatId, limit) {
97
136
  return listThreadMessages(CONNECTOR, chatId, limit);
98
137
  }
99
- export function conversations(userId) {
100
- return listThreadChats(CONNECTOR, chatPrefixFor(userId));
138
+ export async function conversations(viewerId) {
139
+ const chats = await listAllThreadChats(CONNECTOR);
140
+ const states = await getThreadStates(CONNECTOR, chats.map((c) => c.chatId));
141
+ const out = [];
142
+ for (const c of chats) {
143
+ const access = states.get(c.chatId) ?? {
144
+ agentId: null,
145
+ presetId: null,
146
+ title: null,
147
+ owner: null,
148
+ visibility: null,
149
+ };
150
+ if (!mayRead(access, viewerId))
151
+ continue;
152
+ const convId = convIdOf(c.chatId);
153
+ if (convId === null)
154
+ continue;
155
+ out.push({
156
+ convId,
157
+ lastTs: c.lastTs,
158
+ count: c.count,
159
+ firstUserText: c.firstUserText,
160
+ title: access.title,
161
+ owner: access.owner,
162
+ visibility: access.visibility,
163
+ });
164
+ }
165
+ return out;
166
+ }
167
+ export async function openConversation(convId, viewerId, need) {
168
+ if (!convId || convId.includes(':'))
169
+ return null;
170
+ const lookup = await findChatByConvId(CONNECTOR, convId);
171
+ if (lookup.found === 'ambiguous')
172
+ return null;
173
+ if (lookup.found === 'none')
174
+ return chatIdFor(viewerId, convId);
175
+ const access = await getThreadState(CONNECTOR, lookup.chatId);
176
+ const allowed = need === 'read' ? mayRead(access, viewerId) : mayWrite(access, viewerId);
177
+ return allowed ? lookup.chatId : null;
101
178
  }
102
179
  export async function getSelection(chatId) {
103
180
  return getThreadState(CONNECTOR, chatId);
@@ -0,0 +1,9 @@
1
+ export interface Subscriber {
2
+ viewerId: string;
3
+ emit(event: string, data: unknown): void;
4
+ close(): void;
5
+ }
6
+ export declare function subscribe(chatId: string, sub: Subscriber): () => void;
7
+ export declare function broadcast(chatId: string, event: string, data: unknown): void;
8
+ export declare function evictWhere(chatId: string, drop: (viewerId: string) => boolean): void;
9
+ export declare function subscriberCount(chatId: string): number;
@@ -0,0 +1,48 @@
1
+ import { getLogger } from '@coffer-org/sdk/logger';
2
+ const log = getLogger('webchat');
3
+ const channels = new Map();
4
+ export function subscribe(chatId, sub) {
5
+ let set = channels.get(chatId);
6
+ if (!set) {
7
+ set = new Set();
8
+ channels.set(chatId, set);
9
+ }
10
+ set.add(sub);
11
+ return () => {
12
+ const current = channels.get(chatId);
13
+ if (!current)
14
+ return;
15
+ current.delete(sub);
16
+ if (current.size === 0)
17
+ channels.delete(chatId);
18
+ };
19
+ }
20
+ export function broadcast(chatId, event, data) {
21
+ const set = channels.get(chatId);
22
+ if (!set)
23
+ return;
24
+ for (const sub of set) {
25
+ try {
26
+ sub.emit(event, data);
27
+ }
28
+ catch (err) {
29
+ log.warn(`channel emit failed for viewer ${sub.viewerId} on ${chatId}: ${err instanceof Error ? err.message : String(err)}`);
30
+ }
31
+ }
32
+ }
33
+ export function evictWhere(chatId, drop) {
34
+ const set = channels.get(chatId);
35
+ if (!set)
36
+ return;
37
+ for (const sub of set) {
38
+ if (!drop(sub.viewerId))
39
+ continue;
40
+ set.delete(sub);
41
+ sub.close();
42
+ }
43
+ if (set.size === 0)
44
+ channels.delete(chatId);
45
+ }
46
+ export function subscriberCount(chatId) {
47
+ return channels.get(chatId)?.size ?? 0;
48
+ }
@@ -1,6 +1,5 @@
1
1
  export function policy() {
2
2
  return {
3
- accessPassword: '',
4
3
  triggerPrefix: '',
5
4
  replyWindow: 1800,
6
5
  };
@@ -1,5 +1,5 @@
1
1
  import type { Connector } from '@coffer-org/server/orchestrator';
2
- import { recordAssistant as storeAssistant, recordReasoning as storeReasoning, recordSuggestions as storeSuggestions } from './chain-store.ts';
2
+ import { recordAssistant as storeAssistant, recordReasoning as storeReasoning, recordSuggestions as storeSuggestions, recordContext as storeContext, setSelection as storeSelection } from './chain-store.ts';
3
3
  import type { ReasoningDisplay } from './settings.ts';
4
4
  export interface StreamingConnector {
5
5
  connector: Connector;
@@ -9,10 +9,11 @@ export interface StreamingConnector {
9
9
  export interface StreamingConnectorOpts {
10
10
  chatId: string;
11
11
  botMsgId: string;
12
- emit: (event: string, data: unknown) => void;
13
12
  display: ReasoningDisplay;
14
13
  record?: typeof storeAssistant;
15
14
  recordReasoning?: typeof storeReasoning;
16
15
  recordSuggestions?: typeof storeSuggestions;
16
+ recordContext?: typeof storeContext;
17
+ setSelection?: typeof storeSelection;
17
18
  }
18
19
  export declare function makeStreamingConnector(opts: StreamingConnectorOpts): StreamingConnector;
@@ -1,71 +1,106 @@
1
- import { makeLiveChannel, plainRender } from '@coffer-org/server/orchestrator';
2
- import { recordAssistant as storeAssistant, recordReasoning as storeReasoning, recordSuggestions as storeSuggestions, } from "./chain-store.js";
1
+ import { makeLiveSink, plainRender } from '@coffer-org/server/orchestrator';
2
+ import { recordAssistant as storeAssistant, recordReasoning as storeReasoning, recordSuggestions as storeSuggestions, recordContext as storeContext, setSelection as storeSelection, } from "./chain-store.js";
3
+ import { broadcast } from "./channel-registry.js";
4
+ import { getLogger } from '@coffer-org/sdk/logger';
5
+ const log = getLogger('webchat');
3
6
  const WEBCHAT_THROTTLE_MS = 250;
4
7
  const WEBCHAT_MAX = 100_000;
5
8
  export function makeStreamingConnector(opts) {
6
9
  const record = opts.record ?? storeAssistant;
7
10
  const recordReasoningFn = opts.recordReasoning ?? storeReasoning;
8
11
  const recordSuggestionsFn = opts.recordSuggestions ?? storeSuggestions;
12
+ const recordContextFn = opts.recordContext ?? storeContext;
13
+ const setSelectionFn = opts.setSelection ?? storeSelection;
9
14
  let recordedId = null;
10
15
  let lastSuggestions = null;
11
16
  const connector = {
12
17
  id: 'webchat',
13
- reply(_chatId, _ctx) {
14
- const ch = makeLiveChannel({
18
+ enrolmentNotice: 'unreachable: a web-chat turn is always opened by an already-authenticated session, so ' +
19
+ 'the identity gate never refuses one — this connector has no unidentified sender to say it to.',
20
+ open(envelope) {
21
+ const inner = makeLiveSink({
15
22
  ops: {
16
23
  send: async (text) => {
17
- opts.emit('message', { msgId: opts.botMsgId, text });
24
+ broadcast(opts.chatId, 'message', { msgId: opts.botMsgId, text });
18
25
  return opts.botMsgId;
19
26
  },
20
27
  edit: async (msgId, text) => {
21
- opts.emit('delta', { msgId, text });
28
+ broadcast(opts.chatId, 'delta', { msgId, text });
22
29
  },
23
30
  },
24
31
  throttleMs: WEBCHAT_THROTTLE_MS,
25
32
  maxLength: WEBCHAT_MAX,
26
33
  render: plainRender(WEBCHAT_MAX),
27
34
  });
35
+ let answerText = null;
36
+ let answerReasoning = null;
37
+ let hasAnswer = false;
38
+ let persisted = false;
39
+ let afterwordWork = Promise.resolve();
28
40
  return {
29
- ...ch,
30
- async finish(r) {
31
- lastSuggestions = r.suggestions;
32
- return ch.finish(r);
33
- },
34
- updateReasoning(acc) {
35
- if (opts.display === 'off')
41
+ emit(e) {
42
+ if (e.kind === 'reasoning') {
43
+ if (opts.display !== 'off')
44
+ broadcast(opts.chatId, 'reasoning', { msgId: opts.botMsgId, text: e.text });
45
+ return;
46
+ }
47
+ if (e.kind === 'suggestions') {
48
+ lastSuggestions = e.items;
49
+ if (recordedId && e.items.length) {
50
+ void recordSuggestionsFn({
51
+ chatId: opts.chatId,
52
+ botMsgId: opts.botMsgId,
53
+ parentMsgId: envelope.turnId,
54
+ suggestions: e.items,
55
+ now: Math.floor(Date.now() / 1000),
56
+ }).catch((err) => {
57
+ log.warn(`recordSuggestions failed: ${err instanceof Error ? err.message : String(err)}`);
58
+ });
59
+ }
60
+ return;
61
+ }
62
+ if (e.kind === 'title') {
63
+ afterwordWork = afterwordWork.then(() => setSelectionFn(opts.chatId, { title: e.text }).catch((err) => {
64
+ log.warn(`setSelection(title) failed: ${err instanceof Error ? err.message : String(err)}`);
65
+ }));
36
66
  return;
37
- opts.emit('reasoning', { msgId: opts.botMsgId, text: acc });
67
+ }
68
+ if (e.kind === 'answer') {
69
+ answerText = e.text;
70
+ answerReasoning = e.reasoning;
71
+ hasAnswer = true;
72
+ }
73
+ inner.emit(e);
74
+ },
75
+ async done() {
76
+ await inner.done();
77
+ if (hasAnswer && !persisted) {
78
+ persisted = true;
79
+ const now = Math.floor(Date.now() / 1000);
80
+ const wrote = await record({
81
+ chatId: opts.chatId,
82
+ parentMsgId: envelope.turnId,
83
+ botMsgId: opts.botMsgId,
84
+ text: answerText ?? '',
85
+ now,
86
+ });
87
+ recordedId = wrote ? opts.botMsgId : null;
88
+ if (wrote && opts.display !== 'off' && answerReasoning) {
89
+ await recordReasoningFn({
90
+ chatId: opts.chatId,
91
+ botMsgId: opts.botMsgId,
92
+ parentMsgId: envelope.turnId,
93
+ text: answerReasoning,
94
+ now,
95
+ });
96
+ }
97
+ }
98
+ await afterwordWork;
38
99
  },
39
100
  };
40
101
  },
41
- async recordAssistant(m) {
42
- const now = Math.floor(Date.now() / 1000);
43
- const wrote = await record({
44
- chatId: opts.chatId,
45
- parentMsgId: m.parentMsgId,
46
- botMsgId: m.botMsgId,
47
- text: m.text,
48
- now,
49
- });
50
- recordedId = wrote ? m.botMsgId : null;
51
- if (wrote && opts.display !== 'off' && m.reasoning && m.botMsgId) {
52
- await recordReasoningFn({
53
- chatId: opts.chatId,
54
- botMsgId: m.botMsgId,
55
- parentMsgId: m.parentMsgId,
56
- text: m.reasoning,
57
- now,
58
- });
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
- }
102
+ async recordContext(m) {
103
+ await recordContextFn({ ...m, chatId: opts.chatId });
69
104
  },
70
105
  };
71
106
  return {
@@ -2,6 +2,6 @@ import type { PluginHooks } from '@coffer-org/server/plugin-hooks';
2
2
  export type { ThreadSummary, HistoryMsg } from './actions.ts';
3
3
  export { WEB_FORMAT } from './format.ts';
4
4
  export { makeStreamingConnector } from './connector.ts';
5
- export { chatIdFor, chatPrefixFor, recordUser, recordAssistant, buildChain, getSelection, setSelection, } from './chain-store.ts';
5
+ export { chatIdFor, recordUser, recordAssistant, buildChain, getSelection, setSelection } from './chain-store.ts';
6
6
  export { sendAction, pageContext } from './send.ts';
7
7
  export declare const serverHooks: PluginHooks;
@@ -1,6 +1,6 @@
1
1
  import { pruneThreadMessages } from '@coffer-org/server/thread-store';
2
2
  import { pruneThreadState } from '@coffer-org/server/thread-state';
3
- import { threadsAction, historyAction, agentsAction, selectAgentAction } from "./actions.js";
3
+ import { threadsAction, historyAction, agentsAction, selectAgentAction, setVisibilityAction, channelAction, stopAction, } from "./actions.js";
4
4
  import { sendAction } from "./send.js";
5
5
  import { CONNECTOR } from "./chain-store.js";
6
6
  import { registerConnector } from '@coffer-org/server/orchestrator';
@@ -8,7 +8,7 @@ const THREAD_TTL_MS = Number(process.env['WEBCHAT_THREAD_TTL_MS'] ?? 30 * 86_400
8
8
  let unregisterConnector;
9
9
  export { WEB_FORMAT } from "./format.js";
10
10
  export { makeStreamingConnector } from "./connector.js";
11
- export { chatIdFor, chatPrefixFor, recordUser, recordAssistant, buildChain, getSelection, setSelection, } from "./chain-store.js";
11
+ export { chatIdFor, recordUser, recordAssistant, buildChain, getSelection, setSelection } from "./chain-store.js";
12
12
  export { sendAction, pageContext } from "./send.js";
13
13
  export const serverHooks = {
14
14
  init: () => {
@@ -23,9 +23,12 @@ export const serverHooks = {
23
23
  history: historyAction,
24
24
  agents: agentsAction,
25
25
  selectAgent: selectAgentAction,
26
+ setVisibility: (body, caller) => setVisibilityAction(body, caller),
27
+ send: (body, caller) => sendAction(body, caller),
28
+ stop: (body, caller) => stopAction(body, caller),
26
29
  },
27
30
  streamActions: {
28
- send: (body, ctx) => sendAction(body, ctx),
31
+ channel: (body, ctx) => channelAction(body, ctx),
29
32
  },
30
33
  backgroundTasks: [
31
34
  {
@@ -1,12 +1,13 @@
1
1
  import { handleIncoming } from '@coffer-org/server/orchestrator';
2
- import type { ActionCaller } from '@coffer-org/server/plugin-hooks';
2
+ import { type ActionCaller } from '@coffer-org/server/plugin-hooks';
3
+ import type { ConnectorCapabilities, ContextFact } from '@coffer-org/server/orchestrator';
4
+ export declare const CAPABILITIES: ConnectorCapabilities;
3
5
  export interface SendDeps {
4
6
  handleIncoming?: typeof handleIncoming;
5
7
  }
6
- export interface StreamCtx {
7
- caller: ActionCaller;
8
- emit: (event: string, data: unknown) => void;
9
- signal: AbortSignal;
8
+ export declare function pageContext(ctx: unknown): ContextFact[];
9
+ export interface SendResult {
10
+ msgId: string | null;
11
+ botMsgId: string | null;
10
12
  }
11
- export declare function pageContext(ctx: unknown): string;
12
- export declare function sendAction(body: Record<string, unknown>, ctx: StreamCtx, deps?: SendDeps): Promise<void>;
13
+ export declare function sendAction(body: Record<string, unknown>, caller: ActionCaller, deps?: SendDeps): Promise<SendResult>;