@animalabs/agent-framework 0.22.3 → 0.23.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/README.md +105 -3
- package/dist/src/agent.d.ts +36 -10
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +76 -32
- package/dist/src/agent.js.map +1 -1
- package/dist/src/code-execution/index.d.ts +1 -1
- package/dist/src/code-execution/index.d.ts.map +1 -1
- package/dist/src/code-execution/index.js.map +1 -1
- package/dist/src/code-execution/py-runner.d.ts +34 -2
- package/dist/src/code-execution/py-runner.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.js +72 -16
- package/dist/src/code-execution/py-runner.js.map +1 -1
- package/dist/src/code-execution/runtime-py.d.ts +3 -2
- package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
- package/dist/src/code-execution/runtime-py.js +47 -7
- package/dist/src/code-execution/runtime-py.js.map +1 -1
- package/dist/src/code-execution/tool-definition.d.ts +12 -7
- package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
- package/dist/src/code-execution/tool-definition.js +59 -11
- package/dist/src/code-execution/tool-definition.js.map +1 -1
- package/dist/src/config-checks.d.ts +2 -2
- package/dist/src/config-checks.d.ts.map +1 -1
- package/dist/src/config-checks.js +6 -2
- package/dist/src/config-checks.js.map +1 -1
- package/dist/src/framework.d.ts +744 -120
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +3546 -906
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +13 -6
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/held-writes.d.ts +16 -0
- package/dist/src/held-writes.d.ts.map +1 -0
- package/dist/src/held-writes.js +56 -0
- package/dist/src/held-writes.js.map +1 -0
- package/dist/src/index.d.ts +7 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +8 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/capability-grant.d.ts +4 -0
- package/dist/src/mcpl/capability-grant.d.ts.map +1 -1
- package/dist/src/mcpl/capability-grant.js +1 -1
- package/dist/src/mcpl/capability-grant.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +95 -18
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +578 -150
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts +2 -2
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +6 -7
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +5 -1
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +3 -1
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/inference-router.d.ts +4 -2
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +6 -7
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/log-value.d.ts +7 -0
- package/dist/src/mcpl/log-value.d.ts.map +1 -1
- package/dist/src/mcpl/log-value.js +9 -0
- package/dist/src/mcpl/log-value.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +227 -0
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -0
- package/dist/src/mcpl/modern-connection.js +1088 -0
- package/dist/src/mcpl/modern-connection.js.map +1 -0
- package/dist/src/mcpl/prefix-rule.d.ts +18 -0
- package/dist/src/mcpl/prefix-rule.d.ts.map +1 -0
- package/dist/src/mcpl/prefix-rule.js +25 -0
- package/dist/src/mcpl/prefix-rule.js.map +1 -0
- package/dist/src/mcpl/protocol-family.d.ts +179 -0
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -0
- package/dist/src/mcpl/protocol-family.js +697 -0
- package/dist/src/mcpl/protocol-family.js.map +1 -0
- package/dist/src/mcpl/push-coalescer.d.ts +38 -19
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +52 -23
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +66 -15
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
- package/dist/src/mcpl/reference-fetcher.js +243 -76
- package/dist/src/mcpl/reference-fetcher.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +63 -18
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +117 -32
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +326 -30
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +995 -212
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/server-registry.d.ts +25 -5
- package/dist/src/mcpl/server-registry.d.ts.map +1 -1
- package/dist/src/mcpl/server-registry.js +51 -12
- package/dist/src/mcpl/server-registry.js.map +1 -1
- package/dist/src/mcpl/tool-result-normalize.d.ts +54 -0
- package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -0
- package/dist/src/mcpl/tool-result-normalize.js +232 -0
- package/dist/src/mcpl/tool-result-normalize.js.map +1 -0
- package/dist/src/mcpl/transport.d.ts +56 -13
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +206 -70
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +66 -20
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/mcpl/visible-content.d.ts +2 -1
- package/dist/src/mcpl/visible-content.d.ts.map +1 -1
- package/dist/src/mcpl/visible-content.js +2 -1
- package/dist/src/mcpl/visible-content.js.map +1 -1
- package/dist/src/module-registry.d.ts +16 -6
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +65 -6
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.d.ts +1 -1
- package/dist/src/modules/api/index.d.ts.map +1 -1
- package/dist/src/modules/api/index.js +3 -2
- package/dist/src/modules/api/index.js.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts +42 -2
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js +268 -65
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts +27 -5
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +366 -76
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/discord/refusal.d.ts +14 -0
- package/dist/src/modules/discord/refusal.d.ts.map +1 -0
- package/dist/src/modules/discord/refusal.js +14 -0
- package/dist/src/modules/discord/refusal.js.map +1 -0
- package/dist/src/modules/discord/types.d.ts +17 -7
- package/dist/src/modules/discord/types.d.ts.map +1 -1
- package/dist/src/modules/health/index.d.ts.map +1 -1
- package/dist/src/modules/health/index.js +6 -2
- package/dist/src/modules/health/index.js.map +1 -1
- package/dist/src/modules/index.d.ts +1 -1
- package/dist/src/modules/index.d.ts.map +1 -1
- package/dist/src/modules/index.js +1 -1
- package/dist/src/modules/index.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +25 -2
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +45 -5
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +2 -1
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +19 -9
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +214 -42
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +667 -113
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +240 -8
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +554 -24
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +10 -1
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +42 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +130 -25
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/silent-heartbeat.d.ts +112 -39
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +100 -29
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/tool-args.d.ts +40 -0
- package/dist/src/tool-args.d.ts.map +1 -0
- package/dist/src/tool-args.js +207 -0
- package/dist/src/tool-args.js.map +1 -0
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +9 -2
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/types/agent.d.ts +27 -17
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +8 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +17 -2
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +54 -10
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +2 -2
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -1
- package/src/agent.ts +99 -36
- package/src/code-execution/index.ts +1 -0
- package/src/code-execution/py-runner.ts +104 -17
- package/src/code-execution/runtime-py.ts +47 -7
- package/src/code-execution/tool-definition.ts +65 -11
- package/src/config-checks.ts +4 -2
- package/src/framework.ts +3633 -956
- package/src/gate/event-gate.ts +14 -6
- package/src/held-writes.ts +59 -0
- package/src/index.ts +19 -4
- package/src/mcpl/capability-grant.ts +1 -1
- package/src/mcpl/channel-registry.ts +613 -160
- package/src/mcpl/convert-block.ts +15 -7
- package/src/mcpl/index.ts +14 -1
- package/src/mcpl/inference-router.ts +8 -7
- package/src/mcpl/log-value.ts +10 -0
- package/src/mcpl/modern-connection.ts +1182 -0
- package/src/mcpl/prefix-rule.ts +32 -0
- package/src/mcpl/protocol-family.ts +691 -0
- package/src/mcpl/push-coalescer.ts +67 -29
- package/src/mcpl/reference-fetcher.ts +288 -82
- package/src/mcpl/references.ts +123 -33
- package/src/mcpl/server-connection.ts +1050 -221
- package/src/mcpl/server-registry.ts +50 -12
- package/src/mcpl/tool-result-normalize.ts +294 -0
- package/src/mcpl/transport.ts +204 -62
- package/src/mcpl/types.ts +67 -20
- package/src/mcpl/visible-content.ts +2 -1
- package/src/module-registry.ts +68 -13
- package/src/modules/api/index.ts +3 -2
- package/src/modules/discord/discord-js-client.ts +288 -72
- package/src/modules/discord/index.ts +380 -77
- package/src/modules/discord/refusal.ts +13 -0
- package/src/modules/discord/types.ts +17 -9
- package/src/modules/health/index.ts +6 -2
- package/src/modules/index.ts +1 -1
- package/src/modules/workspace/disk-agreement.ts +46 -5
- package/src/modules/workspace/effects.ts +18 -7
- package/src/modules/workspace/index.ts +722 -124
- package/src/modules/workspace/observe.ts +591 -22
- package/src/modules/workspace/types.ts +10 -1
- package/src/modules/workspace/watcher.ts +126 -25
- package/src/silent-heartbeat.ts +184 -59
- package/src/tool-args.ts +213 -0
- package/src/tool-presentation.ts +9 -2
- package/src/types/agent.ts +27 -17
- package/src/types/events.ts +8 -0
- package/src/types/framework.ts +17 -2
- package/src/types/module.ts +55 -10
- package/src/types/trace.ts +2 -2
package/src/module-registry.ts
CHANGED
|
@@ -4,8 +4,6 @@ import type {
|
|
|
4
4
|
MessageId,
|
|
5
5
|
MessageMetadata,
|
|
6
6
|
MessageQuery,
|
|
7
|
-
MessageQueryResult,
|
|
8
|
-
StoredMessage,
|
|
9
7
|
ContextInjection,
|
|
10
8
|
} from '@animalabs/context-manager';
|
|
11
9
|
import type {
|
|
@@ -22,10 +20,14 @@ import type {
|
|
|
22
20
|
SpeechHandlerOptions,
|
|
23
21
|
ProcessEvent,
|
|
24
22
|
TraceEventListener,
|
|
23
|
+
ModuleMessage,
|
|
24
|
+
MessageQueryResult,
|
|
25
25
|
} from './types/index.js';
|
|
26
|
+
import { heldWriteId } from './held-writes.js';
|
|
26
27
|
import type { Agent } from './agent.js';
|
|
27
28
|
import { DEFAULT_SHUTDOWN_TIMEOUT_MS, validateShutdownTimeout, waitForShutdown } from './shutdown.js';
|
|
28
29
|
import { logValue } from './mcpl/log-value.js';
|
|
30
|
+
import { toolArgsProblems } from './tool-args.js';
|
|
29
31
|
|
|
30
32
|
/** How the registry queues a module's event: its push names the module. A
|
|
31
33
|
* `ProcessQueue` serves too, ignoring the name. */
|
|
@@ -111,7 +113,7 @@ export class ModuleRegistry {
|
|
|
111
113
|
private addMessageFn: (participant: string, content: ContentBlock[], metadata?: MessageMetadata) => MessageId;
|
|
112
114
|
private editMessageFn: (id: MessageId, content: ContentBlock[]) => void;
|
|
113
115
|
private removeMessageFn: (id: MessageId) => void;
|
|
114
|
-
private getMessageFn: (id: MessageId) =>
|
|
116
|
+
private getMessageFn: (id: MessageId) => ModuleMessage | null;
|
|
115
117
|
private queryMessagesFn: (filter: MessageQuery) => MessageQueryResult;
|
|
116
118
|
|
|
117
119
|
private pushEventFn: (event: ProcessEvent, moduleName?: string) => void;
|
|
@@ -135,7 +137,7 @@ export class ModuleRegistry {
|
|
|
135
137
|
addMessage: (participant: string, content: ContentBlock[], metadata?: MessageMetadata) => MessageId;
|
|
136
138
|
editMessage: (id: MessageId, content: ContentBlock[]) => void;
|
|
137
139
|
removeMessage: (id: MessageId) => void;
|
|
138
|
-
getMessage: (id: MessageId) =>
|
|
140
|
+
getMessage: (id: MessageId) => ModuleMessage | null;
|
|
139
141
|
queryMessages: (filter: MessageQuery) => MessageQueryResult;
|
|
140
142
|
/** Called with the pushing module's name. */
|
|
141
143
|
pushEvent: (event: ProcessEvent, moduleName?: string) => void;
|
|
@@ -300,8 +302,8 @@ export class ModuleRegistry {
|
|
|
300
302
|
* model. The result is checked and read once, in its JSON form
|
|
301
303
|
* ({@link storableToolResult}).
|
|
302
304
|
*/
|
|
303
|
-
async storableToolCall(call: ToolCall): Promise<ToolResult> {
|
|
304
|
-
return storableToolResult(call, await this.handleToolCall(call));
|
|
305
|
+
async storableToolCall(call: ToolCall, options: { definition?: ToolDefinition } = {}): Promise<ToolResult> {
|
|
306
|
+
return storableToolResult(call, await this.handleToolCall(call, options));
|
|
305
307
|
}
|
|
306
308
|
|
|
307
309
|
/**
|
|
@@ -309,8 +311,14 @@ export class ModuleRegistry {
|
|
|
309
311
|
* result, unchecked, as it returned it, for a caller that uses the result
|
|
310
312
|
* in process (`ModuleContext.callTool`, `executeToolCall`, or a host
|
|
311
313
|
* calling the registry). A module's throw is answered as an error result.
|
|
314
|
+
* A call whose input isn't an object, or lacks a key the tool's schema
|
|
315
|
+
* requires, is answered before the module runs, with an error result that
|
|
316
|
+
* opens "Nothing was done:" and names what to send (toolArgsProblems).
|
|
317
|
+
* A caller that has already resolved the tool's definition, as the `utils`
|
|
318
|
+
* tool has a utility's, passes it as `definition`, and it's checked against
|
|
319
|
+
* that one.
|
|
312
320
|
*/
|
|
313
|
-
async handleToolCall(call: ToolCall): Promise<ToolResult> {
|
|
321
|
+
async handleToolCall(call: ToolCall, options: { definition?: ToolDefinition } = {}): Promise<ToolResult> {
|
|
314
322
|
// Parse module--tool name
|
|
315
323
|
const sepIndex = call.name.indexOf('--');
|
|
316
324
|
if (sepIndex === -1) {
|
|
@@ -343,6 +351,27 @@ export class ModuleRegistry {
|
|
|
343
351
|
};
|
|
344
352
|
|
|
345
353
|
try {
|
|
354
|
+
// The call against the tool's declared schema, before the module runs:
|
|
355
|
+
// a missing required argument is refused with what to send, where the
|
|
356
|
+
// handler would otherwise fail deep inside, or run without it. Values
|
|
357
|
+
// aren't checked here, since a handler may take more than its schema
|
|
358
|
+
// can say (toolArgsProblems). The module's own listing runs inside the
|
|
359
|
+
// try, so its throw is answered like any other. Utilities are listed
|
|
360
|
+
// only for a name the tools don't have, so a utility listing that
|
|
361
|
+
// throws or is slow reaches only calls to such names: a utility called
|
|
362
|
+
// by its own name, and a name the module answers without declaring it.
|
|
363
|
+
// A caller that passes `definition` lists nothing.
|
|
364
|
+
const definition = options.definition
|
|
365
|
+
?? module.getTools().find((tool) => tool.name === toolName)
|
|
366
|
+
?? module.getUtilities?.()?.find((tool) => tool.name === toolName);
|
|
367
|
+
const problems = toolArgsProblems(call.input, definition?.inputSchema);
|
|
368
|
+
if (problems.length > 0) {
|
|
369
|
+
return {
|
|
370
|
+
success: false,
|
|
371
|
+
error: `Nothing was done: ${call.name} ${problems.join('; ')}.`,
|
|
372
|
+
isError: true,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
346
375
|
return await module.handleToolCall(moduleCall);
|
|
347
376
|
} catch (error) {
|
|
348
377
|
return thrownResult(error);
|
|
@@ -583,7 +612,7 @@ class ModuleContextImpl implements ModuleContext {
|
|
|
583
612
|
private addMessageFn: (participant: string, content: ContentBlock[], metadata?: MessageMetadata) => MessageId;
|
|
584
613
|
private editMessageFn: (id: MessageId, content: ContentBlock[]) => void;
|
|
585
614
|
private removeMessageFn: (id: MessageId) => void;
|
|
586
|
-
private getMessageFn: (id: MessageId) =>
|
|
615
|
+
private getMessageFn: (id: MessageId) => ModuleMessage | null;
|
|
587
616
|
private queryMessagesFn: (filter: MessageQuery) => MessageQueryResult;
|
|
588
617
|
private pushEventFn: (event: ProcessEvent, moduleName?: string) => void;
|
|
589
618
|
private onTraceFn: (listener: TraceEventListener) => () => void;
|
|
@@ -603,7 +632,7 @@ class ModuleContextImpl implements ModuleContext {
|
|
|
603
632
|
addMessage: (participant: string, content: ContentBlock[], metadata?: MessageMetadata) => MessageId,
|
|
604
633
|
editMessage: (id: MessageId, content: ContentBlock[]) => void,
|
|
605
634
|
removeMessage: (id: MessageId) => void,
|
|
606
|
-
getMessage: (id: MessageId) =>
|
|
635
|
+
getMessage: (id: MessageId) => ModuleMessage | null,
|
|
607
636
|
queryMessages: (filter: MessageQuery) => MessageQueryResult,
|
|
608
637
|
pushEvent: (event: ProcessEvent, moduleName?: string) => void,
|
|
609
638
|
onTrace: (listener: TraceEventListener) => () => void,
|
|
@@ -730,10 +759,13 @@ class ModuleContextImpl implements ModuleContext {
|
|
|
730
759
|
): MessageId {
|
|
731
760
|
const id = this.addMessageFn(participant, content, metadata);
|
|
732
761
|
|
|
733
|
-
// Track external ID if provided
|
|
762
|
+
// Track external ID if provided. A write the framework holds keeps '',
|
|
763
|
+
// as it did before held writes had handles: the map is module state an
|
|
764
|
+
// earlier release reads back, and a handle is no id to it.
|
|
765
|
+
// findMessageByExternalId finds the held write by its external id.
|
|
734
766
|
if (metadata?.external) {
|
|
735
767
|
const key = `${metadata.external.source}:${metadata.external.id}`;
|
|
736
|
-
this.externalIdMap.set(key, id);
|
|
768
|
+
this.externalIdMap.set(key, heldWriteId(id) === undefined ? id : '');
|
|
737
769
|
// Persist the mapping
|
|
738
770
|
this.persistExternalIdMap();
|
|
739
771
|
}
|
|
@@ -759,10 +791,33 @@ class ModuleContextImpl implements ModuleContext {
|
|
|
759
791
|
|
|
760
792
|
findMessageByExternalId(source: string, externalId: string): MessageId | null {
|
|
761
793
|
const key = `${source}:${externalId}`;
|
|
762
|
-
|
|
794
|
+
const entry = this.externalIdMap.get(key);
|
|
795
|
+
if (entry === undefined) return null;
|
|
796
|
+
// A stored id answers while the store holds its row, and once the row is
|
|
797
|
+
// gone the entry goes, as removeMessage drops it. The '' a held write
|
|
798
|
+
// keeps here (addMessage) names no row, so the message is looked up in
|
|
799
|
+
// the store by its external id, held rows included, and the first row
|
|
800
|
+
// found is taken, as context-manager's findByExternalId takes it: a held
|
|
801
|
+
// row answers by its handle and leaves the entry as it is, and a stored
|
|
802
|
+
// row's id becomes the entry. With nothing found, '' stays, as earlier
|
|
803
|
+
// releases kept it: in a module's start(), the writes the last process
|
|
804
|
+
// held don't show yet, since create() restores them after the modules
|
|
805
|
+
// start, and a write that shows later finds its entry.
|
|
806
|
+
if (entry !== '') {
|
|
807
|
+
if (this.getMessageFn(entry)) return entry;
|
|
808
|
+
this.externalIdMap.delete(key);
|
|
809
|
+
this.persistExternalIdMap();
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
const row = this.queryMessagesFn({ source, externalIds: [externalId] }).messages[0];
|
|
813
|
+
if (row?.held) return row.id;
|
|
814
|
+
if (!row) return null;
|
|
815
|
+
this.externalIdMap.set(key, row.id);
|
|
816
|
+
this.persistExternalIdMap();
|
|
817
|
+
return row.id;
|
|
763
818
|
}
|
|
764
819
|
|
|
765
|
-
getMessage(id: MessageId):
|
|
820
|
+
getMessage(id: MessageId): ModuleMessage | null {
|
|
766
821
|
return this.getMessageFn(id);
|
|
767
822
|
}
|
|
768
823
|
|
package/src/modules/api/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* into conversation messages and inference requests.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { unknownToolMessage } from '../unknown-tool.js';
|
|
8
9
|
import type {
|
|
9
10
|
Module,
|
|
10
11
|
ModuleContext,
|
|
@@ -38,9 +39,9 @@ export class ApiModule implements Module {
|
|
|
38
39
|
// Nothing to clean up
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
async handleToolCall(
|
|
42
|
+
async handleToolCall(call: ToolCall): Promise<ToolResult> {
|
|
42
43
|
// API module has no tools
|
|
43
|
-
return { success: false, error:
|
|
44
|
+
return { success: false, error: unknownToolMessage(this.name, call.name, this.getTools()), isError: true };
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
async onProcess(event: ProcessEvent, _state: ProcessState): Promise<EventResponse> {
|
|
@@ -3,11 +3,18 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
ChannelType,
|
|
6
7
|
Client,
|
|
8
|
+
DiscordAPIError,
|
|
9
|
+
DiscordjsError,
|
|
10
|
+
DiscordjsErrorCodes,
|
|
11
|
+
DiscordjsTypeError,
|
|
7
12
|
GatewayIntentBits,
|
|
8
13
|
Partials,
|
|
14
|
+
RESTJSONErrorCodes,
|
|
9
15
|
TextChannel,
|
|
10
16
|
DMChannel,
|
|
17
|
+
type Channel,
|
|
11
18
|
type Message,
|
|
12
19
|
type Attachment,
|
|
13
20
|
type User,
|
|
@@ -22,6 +29,92 @@ import type {
|
|
|
22
29
|
DiscordChannel,
|
|
23
30
|
HistoryMessage,
|
|
24
31
|
} from './types.js';
|
|
32
|
+
import { DiscordRefusal } from './refusal.js';
|
|
33
|
+
|
|
34
|
+
/** The ids a tool's call named, for the words of Discord's refusal. */
|
|
35
|
+
interface Named {
|
|
36
|
+
channelId?: string;
|
|
37
|
+
messageId?: string;
|
|
38
|
+
guildId?: string;
|
|
39
|
+
userId?: string;
|
|
40
|
+
emoji?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** What a refused call was about, from the ids it named. */
|
|
44
|
+
function target(named: Named): string {
|
|
45
|
+
return named.channelId !== undefined ? `channel ${named.channelId}`
|
|
46
|
+
: named.guildId !== undefined ? `server ${named.guildId}`
|
|
47
|
+
: `user ${named.userId}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Discord's refusal of a request, worded for the agent: what was refused, and
|
|
52
|
+
* the call that shows the right id, or who can change what Discord refused.
|
|
53
|
+
* The codes are discord-api-types' RESTJSONErrorCodes, as discord.js 14.27
|
|
54
|
+
* exports them. A code without a case of its own keeps Discord's own words.
|
|
55
|
+
* `overrides` words a code as one tool means it.
|
|
56
|
+
*/
|
|
57
|
+
function refusalReason(error: DiscordAPIError, named: Named, overrides: Partial<Record<number, string>> = {}): string {
|
|
58
|
+
const override = typeof error.code === 'number' ? overrides[error.code] : undefined;
|
|
59
|
+
if (override !== undefined) return override;
|
|
60
|
+
switch (error.code) {
|
|
61
|
+
case RESTJSONErrorCodes.UnknownChannel:
|
|
62
|
+
return `no ${target(named)} is visible to this bot. The id may be mistyped or remembered rather than copied; discord--list_guilds, then discord--list_channels with a guildId, shows the ids. Threads aren't listed there: a thread started from a message has that message's id, which discord--fetch_history of its channel shows. Nor are direct message channels: one's id is the channelId an earlier discord--dm to that user returned.`;
|
|
63
|
+
case RESTJSONErrorCodes.UnknownGuild:
|
|
64
|
+
return `no server ${named.guildId} is visible to this bot. discord--list_guilds shows the servers it is in, with their ids.`;
|
|
65
|
+
case RESTJSONErrorCodes.UnknownMessage:
|
|
66
|
+
return `channel ${named.channelId} has no message ${named.messageId} that this bot can see. discord--fetch_history with that channelId shows its recent messages and their ids.`;
|
|
67
|
+
case RESTJSONErrorCodes.UnknownUser:
|
|
68
|
+
return `Discord knows no user ${named.userId}. The id may be mistyped or remembered rather than copied; discord--fetch_history of a channel they wrote in shows its messages' authors' ids.`;
|
|
69
|
+
case RESTJSONErrorCodes.UnknownEmoji:
|
|
70
|
+
return unknownEmoji(named);
|
|
71
|
+
case RESTJSONErrorCodes.MissingAccess:
|
|
72
|
+
return `this bot can't see ${target(named)} (Discord answered "Missing Access"), and calling again won't change that. `
|
|
73
|
+
+ 'Ask whoever runs this host to give it access.';
|
|
74
|
+
case RESTJSONErrorCodes.MissingPermissions:
|
|
75
|
+
return `this bot isn't allowed to do that${named.channelId !== undefined || named.guildId !== undefined ? ` in ${target(named)}` : ''} (Discord answered "Missing Permissions"), and calling again won't change that. `
|
|
76
|
+
+ 'Ask whoever runs this host to grant the permission.';
|
|
77
|
+
case RESTJSONErrorCodes.CannotEditMessageAuthoredByAnotherUser:
|
|
78
|
+
return `message ${named.messageId} was written by someone else, and this bot can edit only its own messages.`;
|
|
79
|
+
case RESTJSONErrorCodes.CannotSendMessagesToThisUser:
|
|
80
|
+
return `Discord won't deliver a direct message from this bot ${named.userId !== undefined ? `to user ${named.userId}` : `in channel ${named.channelId}`}, usually because they share no server or don't accept direct messages, and calling again won't change that.`;
|
|
81
|
+
default:
|
|
82
|
+
return `Discord refused the request: ${error.message}`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Discord starts a thread from a message only in a server's text or announcement channel; a forum's or media channel's threads are its posts. */
|
|
87
|
+
function cantHoldThreads(named: Named): string {
|
|
88
|
+
return `${target(named)} can't hold threads. A message can start one only in a server's text or announcement channel.`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A message that has a thread: discord.js finds a message's thread by the message's own id (Message#thread). */
|
|
92
|
+
function existingThread(named: Named): string {
|
|
93
|
+
return `message ${named.messageId} already has a thread, whose id is the message's own: discord--send with channelId ${named.messageId} posts in it.`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function unknownEmoji(named: Named): string {
|
|
97
|
+
return `Discord doesn't know the emoji ${JSON.stringify(named.emoji)}. Use a Unicode emoji, such as "👍", or a custom one as name:id from a server this bot is in.`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* discord.js's own refusal of a call, before it sends any request: a message
|
|
102
|
+
* whose channel can't hold threads, one that already has a thread, or an
|
|
103
|
+
* emoji it can't read. Any other error has no words here.
|
|
104
|
+
*/
|
|
105
|
+
function localRefusalReason(error: unknown, named: Named): string | undefined {
|
|
106
|
+
if (!(error instanceof DiscordjsError) && !(error instanceof DiscordjsTypeError)) return undefined;
|
|
107
|
+
switch ((error as { code?: unknown }).code) {
|
|
108
|
+
case DiscordjsErrorCodes.MessageThreadParent:
|
|
109
|
+
return cantHoldThreads(named);
|
|
110
|
+
case DiscordjsErrorCodes.MessageExistingThread:
|
|
111
|
+
return existingThread(named);
|
|
112
|
+
case DiscordjsErrorCodes.EmojiType:
|
|
113
|
+
return `this bot can't read ${JSON.stringify(named.emoji)} as an emoji, so Discord wasn't asked. Use a Unicode emoji, such as "👍", or a custom one as name:id from a server this bot is in.`;
|
|
114
|
+
default:
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
25
118
|
|
|
26
119
|
export interface DiscordJsClientConfig {
|
|
27
120
|
token: string;
|
|
@@ -47,7 +140,7 @@ export class DiscordJsClient implements DiscordClientInterface {
|
|
|
47
140
|
private channelIds?: string[];
|
|
48
141
|
|
|
49
142
|
private messageHandler?: (message: DiscordMessageData) => void;
|
|
50
|
-
private editHandler?: (messageId: string, newContent: string) => void;
|
|
143
|
+
private editHandler?: (messageId: string, newContent: string, attachments?: DiscordAttachment[]) => void;
|
|
51
144
|
private deleteHandler?: (messageId: string) => void;
|
|
52
145
|
private readyHandler?: () => void;
|
|
53
146
|
|
|
@@ -78,13 +171,18 @@ export class DiscordJsClient implements DiscordClientInterface {
|
|
|
78
171
|
}
|
|
79
172
|
});
|
|
80
173
|
|
|
81
|
-
this.client.on('messageUpdate', (
|
|
174
|
+
this.client.on('messageUpdate', (oldMessage, newMessage) => {
|
|
82
175
|
// content is null for a partial message, one discord.js hadn't cached
|
|
83
176
|
// and whose update carried no text, so there's no text to compare; a
|
|
84
177
|
// cached message keeps the text it had. An edit that leaves no text,
|
|
85
178
|
// beside an attachment, is '' and is still an edit.
|
|
86
179
|
if (typeof newMessage.content !== 'string' || !this.editHandler) return;
|
|
87
|
-
|
|
180
|
+
// Its attachments as the edit left them, for a message discord.js had
|
|
181
|
+
// cached: it takes the update's list when the update carries one and
|
|
182
|
+
// keeps the cached list when it doesn't. A message it hadn't cached
|
|
183
|
+
// starts with an empty list, so one whose update carried none would read
|
|
184
|
+
// as having none; it passes none, and the row's blocks stay.
|
|
185
|
+
this.editHandler(newMessage.id, newMessage.content, oldMessage.partial ? undefined : newMessage.attachments?.map(toAttachment));
|
|
88
186
|
});
|
|
89
187
|
|
|
90
188
|
this.client.on('messageDelete', (message) => {
|
|
@@ -169,35 +267,69 @@ export class DiscordJsClient implements DiscordClientInterface {
|
|
|
169
267
|
channelId: string,
|
|
170
268
|
content: string,
|
|
171
269
|
options?: { replyTo?: string; createThread?: { name: string } }
|
|
172
|
-
): Promise<{ messageId: string; threadFailure?: string }> {
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
reply: options?.replyTo ? { messageReference: options.replyTo } : undefined,
|
|
270
|
+
): Promise<{ messageId: string; threadFailure?: string; note?: string }> {
|
|
271
|
+
const lead = 'Nothing was sent';
|
|
272
|
+
const { textChannel, message } = await this.asked(lead, { channelId }, async () => {
|
|
273
|
+
const textChannel = await this.lookup(lead, `channel ${channelId}`, () => this.messageChannel(channelId, lead, 'send')) as TextChannel | DMChannel;
|
|
274
|
+
const message = await textChannel.send({
|
|
275
|
+
content,
|
|
276
|
+
reply: options?.replyTo ? { messageReference: options.replyTo } : undefined,
|
|
277
|
+
});
|
|
278
|
+
return { textChannel, message };
|
|
182
279
|
});
|
|
183
280
|
|
|
184
|
-
if (options?.createThread
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
// reports why its thread wasn't started.
|
|
190
|
-
return { messageId: message.id, threadFailure: error instanceof Error ? error.message : String(error) };
|
|
191
|
-
}
|
|
281
|
+
if (!options?.createThread) return { messageId: message.id };
|
|
282
|
+
// A channel without threads, such as a DM, gets none, and the send
|
|
283
|
+
// succeeds, as it did; its note says so.
|
|
284
|
+
if (!('threads' in textChannel)) {
|
|
285
|
+
return { messageId: message.id, note: `No thread was started: ${cantHoldThreads({ channelId })}` };
|
|
192
286
|
}
|
|
287
|
+
const threadFailure = await this.threadAfterSend(message, channelId, options.createThread.name);
|
|
288
|
+
return threadFailure === undefined ? { messageId: message.id } : { messageId: message.id, threadFailure };
|
|
289
|
+
}
|
|
193
290
|
|
|
194
|
-
|
|
291
|
+
/**
|
|
292
|
+
* Starts the thread a send asked for. The message is out by then, so when
|
|
293
|
+
* the thread isn't started, why is returned rather than thrown, in words
|
|
294
|
+
* that say the message was sent, lest the agent send it again.
|
|
295
|
+
*/
|
|
296
|
+
private async threadAfterSend(message: Message, channelId: string, name: string): Promise<string | undefined> {
|
|
297
|
+
const sent = `The message was sent (id ${message.id}), so don't send it again`;
|
|
298
|
+
const named = { channelId, messageId: message.id };
|
|
299
|
+
const startOne = `discord--create_thread with channelId ${channelId}, messageId ${message.id} and a name`;
|
|
300
|
+
try {
|
|
301
|
+
await message.startThread({ name });
|
|
302
|
+
return undefined;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (error instanceof DiscordAPIError) {
|
|
305
|
+
// Discord answers a retried start that it had already carried out as
|
|
306
|
+
// a thread that exists: say it's there.
|
|
307
|
+
if (error.code === RESTJSONErrorCodes.ThreadAlreadyCreatedForMessage) return `${sent}, and its thread exists already, with the message's own id, ${message.id}.`;
|
|
308
|
+
// The message is out, and `named` holds it, so no sentence points
|
|
309
|
+
// elsewhere, where posting it again would post it twice.
|
|
310
|
+
// The bot has just posted in the channel, so Missing Access is about
|
|
311
|
+
// its threads, not about seeing it.
|
|
312
|
+
const permission = `this bot isn't allowed to start threads in channel ${channelId} (Discord answered "Missing Permissions"), and calling again won't change that. Ask whoever runs this host to grant the permission.`;
|
|
313
|
+
const access = `this bot can't start threads in channel ${channelId} (Discord answered "Missing Access"), and calling again won't change that. Ask whoever runs this host to give it access.`;
|
|
314
|
+
return `${sent}, but no thread was started: ${refusalReason(error, named, { [RESTJSONErrorCodes.MissingPermissions]: permission, [RESTJSONErrorCodes.MissingAccess]: access })} Once that's settled, ${startOne} starts one.`;
|
|
315
|
+
}
|
|
316
|
+
// The thread refusals localRefusalReason words, a message outside a
|
|
317
|
+
// text or announcement channel and one that has a thread, can't come
|
|
318
|
+
// for a message just sent in a channel that holds threads.
|
|
319
|
+
return `${sent}, but starting its thread stopped on an unexpected error: ${error instanceof Error ? error.message : String(error)}. ` +
|
|
320
|
+
`Whether the thread exists is unknown: discord--fetch_history with channelId ${message.id} reads the thread if it exists and is refused if it doesn't, and then ${startOne} starts one.`;
|
|
321
|
+
}
|
|
195
322
|
}
|
|
196
323
|
|
|
197
|
-
async sendDM(userId: string, content: string): Promise<{ messageId: string }> {
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
324
|
+
async sendDM(userId: string, content: string): Promise<{ messageId: string; channelId: string }> {
|
|
325
|
+
const lead = 'Nothing was sent';
|
|
326
|
+
return this.asked(lead, { userId }, async () => {
|
|
327
|
+
const user = await this.lookup(lead, `user ${userId}`, () => this.client.users.fetch(userId));
|
|
328
|
+
// User#send opens the direct message channel first, which sends nothing.
|
|
329
|
+
const channel = await this.lookup(lead, `the direct message channel with user ${userId}`, () => user.createDM());
|
|
330
|
+
const message = await channel.send(content);
|
|
331
|
+
return { messageId: message.id, channelId: message.channelId };
|
|
332
|
+
});
|
|
201
333
|
}
|
|
202
334
|
|
|
203
335
|
async fetchMessage(channelId: string, messageId: string): Promise<string> {
|
|
@@ -210,30 +342,36 @@ export class DiscordJsClient implements DiscordClientInterface {
|
|
|
210
342
|
}
|
|
211
343
|
|
|
212
344
|
async editMessage(channelId: string, messageId: string, content: string): Promise<void> {
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
await message.edit(content);
|
|
345
|
+
const lead = 'Nothing was edited';
|
|
346
|
+
await this.asked(lead, { channelId, messageId }, async () => {
|
|
347
|
+
const message = await this.findMessage(channelId, messageId, lead);
|
|
348
|
+
await message.edit(content);
|
|
349
|
+
});
|
|
219
350
|
}
|
|
220
351
|
|
|
221
352
|
async deleteMessage(channelId: string, messageId: string): Promise<void> {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
353
|
+
const lead = 'Nothing was deleted';
|
|
354
|
+
try {
|
|
355
|
+
await this.asked(lead, { channelId, messageId }, async () => {
|
|
356
|
+
const message = await this.findMessage(channelId, messageId, lead);
|
|
357
|
+
await message.delete();
|
|
358
|
+
});
|
|
359
|
+
} catch (error) {
|
|
360
|
+
// A message that isn't there may be one an earlier call deleted, or a
|
|
361
|
+
// retried request of this one, so its sentence has no lead.
|
|
362
|
+
if (error instanceof DiscordRefusal && error.cause instanceof DiscordAPIError && error.cause.code === RESTJSONErrorCodes.UnknownMessage) {
|
|
363
|
+
throw new DiscordRefusal(`Channel ${channelId} has no message ${messageId} now: it's deleted, or the id is wrong. discord--fetch_history with that channelId shows its recent messages and their ids.`, { cause: error.cause });
|
|
364
|
+
}
|
|
365
|
+
throw error;
|
|
225
366
|
}
|
|
226
|
-
const message = await (channel as TextChannel).messages.fetch(messageId);
|
|
227
|
-
await message.delete();
|
|
228
367
|
}
|
|
229
368
|
|
|
230
369
|
async addReaction(channelId: string, messageId: string, emoji: string): Promise<void> {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
await message.react(emoji);
|
|
370
|
+
const lead = 'No reaction was added';
|
|
371
|
+
await this.asked(lead, { channelId, messageId, emoji }, async () => {
|
|
372
|
+
const message = await this.findMessage(channelId, messageId, lead);
|
|
373
|
+
await message.react(emoji);
|
|
374
|
+
});
|
|
237
375
|
}
|
|
238
376
|
|
|
239
377
|
async createThread(
|
|
@@ -242,23 +380,108 @@ export class DiscordJsClient implements DiscordClientInterface {
|
|
|
242
380
|
name: string,
|
|
243
381
|
autoArchiveDuration?: number
|
|
244
382
|
): Promise<{ threadId: string }> {
|
|
383
|
+
const lead = 'No thread was started';
|
|
384
|
+
try {
|
|
385
|
+
return await this.asked(lead, { channelId, messageId }, async () => {
|
|
386
|
+
const message = await this.findMessage(channelId, messageId, lead);
|
|
387
|
+
const thread = await message.startThread({
|
|
388
|
+
name,
|
|
389
|
+
autoArchiveDuration: (autoArchiveDuration as 60 | 1440 | 4320 | 10080) ?? 1440,
|
|
390
|
+
});
|
|
391
|
+
return { threadId: thread.id };
|
|
392
|
+
});
|
|
393
|
+
} catch (error) {
|
|
394
|
+
// Discord's answer for a message that has a thread, whether an earlier
|
|
395
|
+
// call started it or a retried request of this one did.
|
|
396
|
+
if (error instanceof DiscordRefusal && error.cause instanceof DiscordAPIError && error.cause.code === RESTJSONErrorCodes.ThreadAlreadyCreatedForMessage) {
|
|
397
|
+
throw new DiscordRefusal(`Message ${messageId} has a thread already, whose id is the message's own: discord--send with channelId ${messageId} posts in it.`, { cause: error.cause });
|
|
398
|
+
}
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* The message a tool acts on, looked up in its channel. An empty id is
|
|
405
|
+
* refused before any request: discord.js fetches a channel's recent
|
|
406
|
+
* messages for one, not a message.
|
|
407
|
+
*/
|
|
408
|
+
private findMessage(channelId: string, messageId: string, lead: string): Promise<Message> {
|
|
409
|
+
if (!messageId) {
|
|
410
|
+
return Promise.reject(new DiscordRefusal(`${lead}: the call gave no message id. discord--fetch_history with channelId ${channelId} shows its recent messages and their ids.`));
|
|
411
|
+
}
|
|
412
|
+
return this.lookup(lead, `message ${messageId} in channel ${channelId}`,
|
|
413
|
+
async () => (await this.messageChannel(channelId, lead, 'messages')).messages.fetch(messageId));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Runs a lookup a tool makes before its change. A lookup sends, edits and
|
|
418
|
+
* removes nothing, so one that stops on an unexpected error, such as a
|
|
419
|
+
* network failure, means the tool did nothing, which a refusal led by
|
|
420
|
+
* `lead` says, and calling it again is safe: opening a direct message
|
|
421
|
+
* channel, the one lookup that can make something on Discord, opens the
|
|
422
|
+
* same channel again. Discord refusing it, or a refusal of this module's
|
|
423
|
+
* own, passes on as it is; anything else, discord.js's own errors
|
|
424
|
+
* included, is the unexpected stop.
|
|
425
|
+
*/
|
|
426
|
+
private async lookup<T>(lead: string, what: string, run: () => Promise<T>): Promise<T> {
|
|
427
|
+
try {
|
|
428
|
+
return await run();
|
|
429
|
+
} catch (error) {
|
|
430
|
+
if (error instanceof DiscordAPIError || error instanceof DiscordRefusal) throw error;
|
|
431
|
+
throw new DiscordRefusal(`${lead}: looking up ${what} stopped on an unexpected error: ${error instanceof Error ? error.message : String(error)}. Calling it again is safe.`, { cause: error });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Runs one tool's requests. Discord refusing any of them, which discord.js
|
|
437
|
+
* reports as a DiscordAPIError, becomes a DiscordRefusal led by `lead`, with
|
|
438
|
+
* Discord's error as its cause: a tool's change is its last request, so it
|
|
439
|
+
* wasn't made. So does discord.js refusing a call before it sends one
|
|
440
|
+
* (localRefusalReason). Anything else passes through as an unexpected error.
|
|
441
|
+
*/
|
|
442
|
+
private async asked<T>(lead: string, named: Named, run: () => Promise<T>): Promise<T> {
|
|
443
|
+
try {
|
|
444
|
+
return await run();
|
|
445
|
+
} catch (error) {
|
|
446
|
+
if (error instanceof DiscordAPIError) throw new DiscordRefusal(`${lead}: ${refusalReason(error, named)}`, { cause: error });
|
|
447
|
+
const local = localRefusalReason(error, named);
|
|
448
|
+
if (local !== undefined) throw new DiscordRefusal(`${lead}: ${local}`, { cause: error });
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* The channel `channelId` names, when it holds messages (and, with 'send',
|
|
455
|
+
* takes them). A channel Discord knows but that holds none, such as a
|
|
456
|
+
* category, is refused with what it is.
|
|
457
|
+
*/
|
|
458
|
+
private async messageChannel(channelId: string, lead: string, need: 'messages' | 'send'): Promise<TextChannel> {
|
|
245
459
|
const channel = await this.client.channels.fetch(channelId);
|
|
246
|
-
if (!channel || !(
|
|
247
|
-
throw new
|
|
460
|
+
if (!channel || !(need in channel)) {
|
|
461
|
+
throw new DiscordRefusal(`${lead}: ${this.holdsNoMessages(channelId, channel)}`);
|
|
248
462
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
463
|
+
return channel as TextChannel;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private holdsNoMessages(channelId: string, channel: Channel | null): string {
|
|
467
|
+
if (!channel) {
|
|
468
|
+
return `this bot can't use channel ${channelId}: Discord answered for it, but it's a kind of channel this bot doesn't handle, or its server isn't loaded here, and calling again won't change that. Ask whoever runs this host if you need it.`;
|
|
469
|
+
}
|
|
470
|
+
const type = this.mapChannelType(channel.type);
|
|
471
|
+
const inPosts = (kind: string) => `${kind}, whose messages are in its posts, each a thread with its own id, which this module has no tool to list`;
|
|
472
|
+
const is = type === 'category' ? 'a category, which holds channels, not messages'
|
|
473
|
+
: type === 'forum' ? inPosts('a forum')
|
|
474
|
+
: channel.type === ChannelType.GuildMedia ? inPosts('a media channel')
|
|
475
|
+
: type === 'unknown' ? 'a kind of channel that holds no messages'
|
|
476
|
+
: `a ${type} channel, which holds no messages`;
|
|
477
|
+
return `channel ${channelId} is ${is}. discord--list_channels with a guildId shows the server's channels and their ids.`;
|
|
255
478
|
}
|
|
256
479
|
|
|
257
480
|
onMessage(handler: (message: DiscordMessageData) => void): void {
|
|
258
481
|
this.messageHandler = handler;
|
|
259
482
|
}
|
|
260
483
|
|
|
261
|
-
onMessageEdit(handler: (messageId: string, newContent: string) => void): void {
|
|
484
|
+
onMessageEdit(handler: (messageId: string, newContent: string, attachments?: DiscordAttachment[]) => void): void {
|
|
262
485
|
this.editHandler = handler;
|
|
263
486
|
}
|
|
264
487
|
|
|
@@ -293,17 +516,12 @@ export class DiscordJsClient implements DiscordClientInterface {
|
|
|
293
516
|
}
|
|
294
517
|
|
|
295
518
|
async listChannels(guildId: string): Promise<DiscordChannel[]> {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (!guild.channels) {
|
|
303
|
-
throw new Error(`Guild ${guildId} channels not available - may need to re-fetch with force`);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const channels = await guild.channels.fetch();
|
|
519
|
+
// An unknown or unreachable server is Discord's refusal (fetch throws);
|
|
520
|
+
// a fetched Guild always carries its channel manager.
|
|
521
|
+
const channels = await this.asked('Nothing was listed', { guildId }, async () => {
|
|
522
|
+
const guild = await this.client.guilds.fetch(guildId);
|
|
523
|
+
return guild.channels.fetch();
|
|
524
|
+
});
|
|
307
525
|
const result: DiscordChannel[] = [];
|
|
308
526
|
|
|
309
527
|
channels.forEach((c: GuildBasedChannel | null) => {
|
|
@@ -339,15 +557,13 @@ export class DiscordJsClient implements DiscordClientInterface {
|
|
|
339
557
|
channelId: string,
|
|
340
558
|
options?: { limit?: number; before?: string }
|
|
341
559
|
): Promise<HistoryMessage[]> {
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
limit: options?.limit ?? 50,
|
|
350
|
-
before: options?.before,
|
|
560
|
+
const lead = 'No history was read';
|
|
561
|
+
const messages = await this.asked(lead, { channelId, messageId: options?.before }, async () => {
|
|
562
|
+
const textChannel = await this.messageChannel(channelId, lead, 'messages');
|
|
563
|
+
return textChannel.messages.fetch({
|
|
564
|
+
limit: options?.limit ?? 50,
|
|
565
|
+
before: options?.before,
|
|
566
|
+
});
|
|
351
567
|
});
|
|
352
568
|
|
|
353
569
|
return messages.map((m: Message) => ({
|