@andreprado/agentkit 0.1.0-alpha.14 → 0.1.0-alpha.16
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 +2 -0
- package/docs/guides/add-channel.md +63 -0
- package/docs/guides/channel-security.md +32 -0
- package/docs/guides/connect-telegram.md +58 -0
- package/docs/guides/connect-whatsapp-zapster.md +65 -0
- package/docs/guides/run-evals.md +73 -25
- package/docs/llms-full.txt +24 -9
- package/docs/llms.txt +2 -0
- package/package.json +1 -1
- package/src/cli/cloud-client.ts +30 -10
- package/src/cli/commands/channels.ts +2 -0
- package/src/cli/deploy-readiness.ts +32 -11
- package/src/cli/index.ts +20 -6
- package/src/cloud/client.ts +4 -3
- package/src/cloud/contracts.ts +1 -1
- package/src/create-project.ts +1 -1
- package/src/index.ts +110 -1
- package/src/providers/pi.ts +14 -1
- package/src/providers/test.ts +36 -0
- package/src/runtime/channel-test-harness.ts +2 -0
- package/src/runtime/channels/telegram.ts +326 -10
- package/src/runtime/channels/whatsapp-zapster.ts +319 -0
- package/src/runtime/channels.ts +47 -1
- package/src/runtime/chat.ts +59 -42
- package/src/runtime/config.ts +96 -4
- package/src/runtime/core/manifest.ts +35 -3
- package/src/runtime/deploy-readiness.ts +3 -3
- package/src/runtime/dev-server.ts +243 -17
- package/src/runtime/env.ts +8 -3
- package/src/runtime/evals.ts +404 -69
- package/src/runtime/inspect.ts +46 -0
- package/src/runtime/prompt-context.ts +141 -0
- package/src/runtime/runtime-contract.ts +17 -7
- package/src/runtime/targets/cloudflare/build.ts +25 -3
- package/src/runtime/targets/container/server.ts +1 -1
- package/src/runtime/targets/vps/deploy.ts +25 -8
- package/src/runtime/tool-runner.ts +7 -0
- package/src/runtime/tools.ts +8 -2
- package/src/runtime/transcription.ts +483 -0
- package/src/templates/blank.ts +8 -3
- package/src/templates/dentista.ts +18 -10
- package/src/templates/skills/agentkit-build-agent/SKILL.md +6 -5
- package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +2 -1
- package/src/templates/skills/agentkit-capsule/SKILL.md +1 -1
- package/src/templates/skills/agentkit-channels/SKILL.md +34 -1
- package/src/templates/skills/agentkit-channels/references/channel-debugging.md +13 -0
- package/src/templates/skills/agentkit-channels/references/telegram.md +32 -0
- package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +29 -0
- package/src/templates/skills/agentkit-evals/SKILL.md +53 -13
- package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +13 -6
- package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +8 -4
- package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +8 -4
- package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +16 -7
- package/src/templates/skills/agentkit-prompts/SKILL.md +3 -1
- package/src/templates/skills/agentkit-tools/SKILL.md +2 -1
- package/src/templates/support.ts +8 -3
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ChannelAdapter,
|
|
3
|
+
ChannelMediaDownloadInput,
|
|
4
|
+
ChannelMediaDownloadResult,
|
|
3
5
|
ChannelSendInput,
|
|
4
6
|
ChannelSendResult,
|
|
5
7
|
RawWebhookEvent,
|
|
@@ -105,6 +107,22 @@ export const zapsterWhatsappChannelAdapter: ChannelAdapter = {
|
|
|
105
107
|
},
|
|
106
108
|
};
|
|
107
109
|
|
|
110
|
+
if (messageType === "audio" && message.audio) {
|
|
111
|
+
return [
|
|
112
|
+
{
|
|
113
|
+
...base,
|
|
114
|
+
kind: "message",
|
|
115
|
+
message: {
|
|
116
|
+
role: "user",
|
|
117
|
+
contentType: "audio",
|
|
118
|
+
content: "",
|
|
119
|
+
providerMessageId: message.id,
|
|
120
|
+
audio: message.audio,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
|
|
108
126
|
if (messageType !== "text") {
|
|
109
127
|
return [
|
|
110
128
|
{
|
|
@@ -140,6 +158,9 @@ export const zapsterWhatsappChannelAdapter: ChannelAdapter = {
|
|
|
140
158
|
},
|
|
141
159
|
];
|
|
142
160
|
},
|
|
161
|
+
async downloadMedia(input) {
|
|
162
|
+
return downloadZapsterMedia(input);
|
|
163
|
+
},
|
|
143
164
|
async sendMessage(input) {
|
|
144
165
|
return sendZapsterMessage(input);
|
|
145
166
|
},
|
|
@@ -169,6 +190,15 @@ type NormalizedZapsterWebhookMessage = {
|
|
|
169
190
|
contactId?: string;
|
|
170
191
|
type?: string;
|
|
171
192
|
text?: string;
|
|
193
|
+
audio?: {
|
|
194
|
+
providerMediaId: string;
|
|
195
|
+
kind: "voice" | "audio";
|
|
196
|
+
mimeType?: string;
|
|
197
|
+
filename?: string;
|
|
198
|
+
durationSeconds?: number;
|
|
199
|
+
sizeBytes?: number;
|
|
200
|
+
downloadUrl?: string;
|
|
201
|
+
};
|
|
172
202
|
};
|
|
173
203
|
|
|
174
204
|
function normalizeZapsterOfficialMessage(body: Record<string, unknown>): NormalizedZapsterWebhookMessage | null {
|
|
@@ -185,17 +215,48 @@ function normalizeZapsterOfficialMessage(body: Record<string, unknown>): Normali
|
|
|
185
215
|
contactId: readString(sender, "id"),
|
|
186
216
|
type: readString(data, "type"),
|
|
187
217
|
text: readString(content, "text"),
|
|
218
|
+
audio: normalizeZapsterAudio(content),
|
|
188
219
|
};
|
|
189
220
|
}
|
|
190
221
|
|
|
191
222
|
function normalizeZapsterLegacyMessage(body: Record<string, unknown>): NormalizedZapsterWebhookMessage {
|
|
192
223
|
const message = readRecord(body, "message");
|
|
224
|
+
const audio = readRecord(message, "audio");
|
|
193
225
|
|
|
194
226
|
return {
|
|
195
227
|
id: readString(message, "id"),
|
|
196
228
|
contactId: readString(message, "from"),
|
|
197
229
|
type: readString(message, "type"),
|
|
198
230
|
text: readString(readRecord(message, "text"), "body"),
|
|
231
|
+
audio: normalizeZapsterAudio(audio),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function normalizeZapsterAudio(value: Record<string, unknown>): NormalizedZapsterWebhookMessage["audio"] {
|
|
236
|
+
const mediaId = readString(value, "id") ?? readString(value, "media_id") ?? readString(value, "mediaId");
|
|
237
|
+
const downloadUrl =
|
|
238
|
+
readString(value, "url") ??
|
|
239
|
+
readString(value, "download_url") ??
|
|
240
|
+
readString(value, "downloadUrl") ??
|
|
241
|
+
readString(value, "media_url") ??
|
|
242
|
+
readString(value, "mediaUrl");
|
|
243
|
+
|
|
244
|
+
if (!mediaId && !downloadUrl) {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
providerMediaId: mediaId ?? downloadUrl ?? "unknown",
|
|
250
|
+
kind: readString(value, "ptt") === "true" ? "voice" : "audio",
|
|
251
|
+
...(readString(value, "mime_type") ?? readString(value, "mimeType")
|
|
252
|
+
? { mimeType: readString(value, "mime_type") ?? readString(value, "mimeType") }
|
|
253
|
+
: {}),
|
|
254
|
+
filename: readString(value, "filename") ?? readString(value, "file_name") ?? defaultZapsterAudioFilename(value),
|
|
255
|
+
...(readNumber(value, "duration") !== undefined ? { durationSeconds: readNumber(value, "duration") } : {}),
|
|
256
|
+
...(readNumber(value, "file_size") ?? readNumber(value, "size")
|
|
257
|
+
? { sizeBytes: readNumber(value, "file_size") ?? readNumber(value, "size") }
|
|
258
|
+
: {}),
|
|
259
|
+
...(downloadUrl ? { downloadUrl } : {}),
|
|
199
260
|
};
|
|
200
261
|
}
|
|
201
262
|
|
|
@@ -249,6 +310,8 @@ export type ZapsterSendMessageRequest = {
|
|
|
249
310
|
type ZapsterFetch = (url: string, init: RequestInit) => Promise<Response>;
|
|
250
311
|
|
|
251
312
|
const ZAPSTER_SEND_MESSAGE_URL = "https://api.zapsterapi.com/v1/wa/messages";
|
|
313
|
+
const ZAPSTER_DEFAULT_MEDIA_HOSTS = ["api.zapsterapi.com"];
|
|
314
|
+
const ZAPSTER_MAX_DOWNLOAD_BYTES = 25_000_000;
|
|
252
315
|
|
|
253
316
|
export function buildZapsterSendMessageRequest(input: ChannelSendInput): ZapsterSendMessageRequest {
|
|
254
317
|
return buildZapsterSendMessageRequestInternal(input, "<redacted>");
|
|
@@ -381,6 +444,239 @@ export async function sendZapsterMessage(
|
|
|
381
444
|
};
|
|
382
445
|
}
|
|
383
446
|
|
|
447
|
+
export async function downloadZapsterMedia(
|
|
448
|
+
input: ChannelMediaDownloadInput,
|
|
449
|
+
fetcher: ZapsterFetch = fetch,
|
|
450
|
+
): Promise<ChannelMediaDownloadResult> {
|
|
451
|
+
const apiKey = input.secrets.ZAPSTER_API_KEY;
|
|
452
|
+
const audio = input.message.audio;
|
|
453
|
+
|
|
454
|
+
if (!apiKey) {
|
|
455
|
+
return {
|
|
456
|
+
ok: false,
|
|
457
|
+
retryable: false,
|
|
458
|
+
code: "channel_secret_missing",
|
|
459
|
+
message: "Secret ZAPSTER_API_KEY is missing.",
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (!audio?.downloadUrl) {
|
|
464
|
+
return {
|
|
465
|
+
ok: false,
|
|
466
|
+
retryable: false,
|
|
467
|
+
code: "channel_audio_download_unavailable",
|
|
468
|
+
message: "Zapster audio payload did not include a media download URL.",
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const mediaUrl = validateZapsterMediaDownloadUrl(audio.downloadUrl);
|
|
473
|
+
|
|
474
|
+
if (!mediaUrl.ok) {
|
|
475
|
+
return mediaUrl;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
let response: Response;
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
response = await fetcher(mediaUrl.url, {
|
|
482
|
+
method: "GET",
|
|
483
|
+
headers: {
|
|
484
|
+
Authorization: `Bearer ${apiKey}`,
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
} catch (error) {
|
|
488
|
+
return {
|
|
489
|
+
ok: false,
|
|
490
|
+
retryable: true,
|
|
491
|
+
code: "channel_provider_unavailable",
|
|
492
|
+
message: `Zapster media download failed before a response: ${redactString(
|
|
493
|
+
error instanceof Error ? error.message : String(error),
|
|
494
|
+
[apiKey, mediaUrl.url],
|
|
495
|
+
)}`,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (!response.ok) {
|
|
500
|
+
return {
|
|
501
|
+
ok: false,
|
|
502
|
+
retryable: response.status === 429 || response.status >= 500,
|
|
503
|
+
code: response.status === 429 || response.status >= 500
|
|
504
|
+
? "channel_provider_unavailable"
|
|
505
|
+
: "channel_media_download_failed",
|
|
506
|
+
message: `Zapster media download returned HTTP ${response.status}.`,
|
|
507
|
+
providerMetadata: {
|
|
508
|
+
status: response.status,
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const maxDownloadBytes = input.maxBytes ?? ZAPSTER_MAX_DOWNLOAD_BYTES;
|
|
514
|
+
const bytes = await readResponseBytesWithLimit(response, maxDownloadBytes);
|
|
515
|
+
|
|
516
|
+
if (!bytes.ok) {
|
|
517
|
+
return bytes;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
ok: true,
|
|
522
|
+
audio: bytes.audio,
|
|
523
|
+
filename: audio.filename ?? `${audio.providerMediaId}.ogg`,
|
|
524
|
+
...(audio.mimeType ? { mimeType: audio.mimeType } : {}),
|
|
525
|
+
...(audio.durationSeconds !== undefined ? { durationSeconds: audio.durationSeconds } : {}),
|
|
526
|
+
sizeBytes: bytes.audio.byteLength,
|
|
527
|
+
providerMetadata: {
|
|
528
|
+
status: response.status,
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function validateZapsterMediaDownloadUrl(rawUrl: string):
|
|
534
|
+
| {
|
|
535
|
+
ok: true;
|
|
536
|
+
url: string;
|
|
537
|
+
}
|
|
538
|
+
| {
|
|
539
|
+
ok: false;
|
|
540
|
+
retryable: false;
|
|
541
|
+
code: "channel_audio_download_unavailable";
|
|
542
|
+
message: string;
|
|
543
|
+
} {
|
|
544
|
+
let url: URL;
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
url = new URL(rawUrl);
|
|
548
|
+
} catch {
|
|
549
|
+
return {
|
|
550
|
+
ok: false,
|
|
551
|
+
retryable: false,
|
|
552
|
+
code: "channel_audio_download_unavailable",
|
|
553
|
+
message: "Zapster audio download URL is not a valid URL.",
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (url.protocol !== "https:") {
|
|
558
|
+
return {
|
|
559
|
+
ok: false,
|
|
560
|
+
retryable: false,
|
|
561
|
+
code: "channel_audio_download_unavailable",
|
|
562
|
+
message: "Zapster audio download URL must use HTTPS.",
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const hostname = url.hostname.toLowerCase();
|
|
567
|
+
const allowed = ZAPSTER_DEFAULT_MEDIA_HOSTS.some((host) => hostname === host || hostname.endsWith(`.${host}`));
|
|
568
|
+
|
|
569
|
+
if (!allowed) {
|
|
570
|
+
return {
|
|
571
|
+
ok: false,
|
|
572
|
+
retryable: false,
|
|
573
|
+
code: "channel_audio_download_unavailable",
|
|
574
|
+
message: "Zapster audio download URL host is not allowed.",
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
url.username = "";
|
|
579
|
+
url.password = "";
|
|
580
|
+
|
|
581
|
+
return {
|
|
582
|
+
ok: true,
|
|
583
|
+
url: url.href,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function readResponseBytesWithLimit(
|
|
588
|
+
response: Response,
|
|
589
|
+
maxBytes: number,
|
|
590
|
+
): Promise<
|
|
591
|
+
| {
|
|
592
|
+
ok: true;
|
|
593
|
+
audio: Uint8Array;
|
|
594
|
+
}
|
|
595
|
+
| {
|
|
596
|
+
ok: false;
|
|
597
|
+
retryable: false;
|
|
598
|
+
code: "channel_audio_too_large";
|
|
599
|
+
message: string;
|
|
600
|
+
providerMetadata?: Record<string, unknown>;
|
|
601
|
+
}
|
|
602
|
+
> {
|
|
603
|
+
const contentLength = response.headers.get("content-length");
|
|
604
|
+
const declaredBytes = contentLength ? Number(contentLength) : undefined;
|
|
605
|
+
|
|
606
|
+
if (declaredBytes !== undefined && Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
|
|
607
|
+
return {
|
|
608
|
+
ok: false,
|
|
609
|
+
retryable: false,
|
|
610
|
+
code: "channel_audio_too_large",
|
|
611
|
+
message: `Audio response declares ${declaredBytes} bytes, which exceeds the configured ${maxBytes} byte limit.`,
|
|
612
|
+
providerMetadata: {
|
|
613
|
+
sizeBytes: declaredBytes,
|
|
614
|
+
},
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (!response.body) {
|
|
619
|
+
const audio = new Uint8Array(await response.arrayBuffer());
|
|
620
|
+
|
|
621
|
+
if (audio.byteLength > maxBytes) {
|
|
622
|
+
return {
|
|
623
|
+
ok: false,
|
|
624
|
+
retryable: false,
|
|
625
|
+
code: "channel_audio_too_large",
|
|
626
|
+
message: `Audio response is ${audio.byteLength} bytes, which exceeds the configured ${maxBytes} byte limit.`,
|
|
627
|
+
providerMetadata: {
|
|
628
|
+
sizeBytes: audio.byteLength,
|
|
629
|
+
},
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return { ok: true, audio };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const reader = response.body.getReader();
|
|
637
|
+
const chunks: Uint8Array[] = [];
|
|
638
|
+
let totalBytes = 0;
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
while (true) {
|
|
642
|
+
const chunk = await reader.read();
|
|
643
|
+
|
|
644
|
+
if (chunk.done) {
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
totalBytes += chunk.value.byteLength;
|
|
649
|
+
|
|
650
|
+
if (totalBytes > maxBytes) {
|
|
651
|
+
await reader.cancel().catch(() => undefined);
|
|
652
|
+
return {
|
|
653
|
+
ok: false,
|
|
654
|
+
retryable: false,
|
|
655
|
+
code: "channel_audio_too_large",
|
|
656
|
+
message: `Audio response exceeded the configured ${maxBytes} byte limit while downloading.`,
|
|
657
|
+
providerMetadata: {
|
|
658
|
+
sizeBytes: totalBytes,
|
|
659
|
+
},
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
chunks.push(chunk.value);
|
|
664
|
+
}
|
|
665
|
+
} finally {
|
|
666
|
+
reader.releaseLock();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const audio = new Uint8Array(totalBytes);
|
|
670
|
+
let offset = 0;
|
|
671
|
+
|
|
672
|
+
for (const chunk of chunks) {
|
|
673
|
+
audio.set(chunk, offset);
|
|
674
|
+
offset += chunk.byteLength;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return { ok: true, audio };
|
|
678
|
+
}
|
|
679
|
+
|
|
384
680
|
function buildZapsterSendMessageRequestInternal(
|
|
385
681
|
input: ChannelSendInput,
|
|
386
682
|
instanceId: string,
|
|
@@ -494,6 +790,29 @@ function readString(value: Record<string, unknown>, key: string): string | undef
|
|
|
494
790
|
return typeof field === "string" && field.length > 0 ? field : undefined;
|
|
495
791
|
}
|
|
496
792
|
|
|
793
|
+
function readNumber(value: Record<string, unknown>, key: string): number | undefined {
|
|
794
|
+
const field = value[key];
|
|
795
|
+
return typeof field === "number" && Number.isFinite(field) ? field : undefined;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function defaultZapsterAudioFilename(value: Record<string, unknown>): string {
|
|
799
|
+
const mimeType = readString(value, "mime_type") ?? readString(value, "mimeType");
|
|
800
|
+
|
|
801
|
+
if (mimeType?.includes("ogg")) {
|
|
802
|
+
return "audio.ogg";
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (mimeType?.includes("mpeg") || mimeType?.includes("mp3")) {
|
|
806
|
+
return "audio.mp3";
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (mimeType?.includes("wav")) {
|
|
810
|
+
return "audio.wav";
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return "audio.ogg";
|
|
814
|
+
}
|
|
815
|
+
|
|
497
816
|
function isPositiveIntegerHeader(value: string | null): value is string {
|
|
498
817
|
return typeof value === "string" && /^[1-9]\d*$/.test(value);
|
|
499
818
|
}
|
package/src/runtime/channels.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type { AgentChannel, ChannelProvider, ChannelType } from "../index";
|
|
2
2
|
|
|
3
3
|
export type ChannelMessageRole = "user" | "assistant";
|
|
4
|
-
export type ChannelMessageContentType = "text";
|
|
4
|
+
export type ChannelMessageContentType = "text" | "audio";
|
|
5
5
|
export type ChannelEventKind = "message" | "delivery_status" | "unsupported";
|
|
6
6
|
export type ChannelDeliveryDirection = "inbound" | "outbound";
|
|
7
7
|
export type ChannelDeliveryState =
|
|
8
8
|
| "webhook_received"
|
|
9
9
|
| "received"
|
|
10
10
|
| "validated"
|
|
11
|
+
| "audio_received"
|
|
12
|
+
| "audio_downloaded"
|
|
13
|
+
| "transcribing"
|
|
14
|
+
| "transcribed"
|
|
11
15
|
| "duplicate"
|
|
12
16
|
| "buffered"
|
|
13
17
|
| "queued"
|
|
@@ -45,11 +49,22 @@ export type ChannelExternalIdentity = {
|
|
|
45
49
|
redactedDisplay?: string;
|
|
46
50
|
};
|
|
47
51
|
|
|
52
|
+
export type ChannelAudioAttachment = {
|
|
53
|
+
providerMediaId: string;
|
|
54
|
+
kind: "voice" | "audio";
|
|
55
|
+
mimeType?: string;
|
|
56
|
+
filename?: string;
|
|
57
|
+
durationSeconds?: number;
|
|
58
|
+
sizeBytes?: number;
|
|
59
|
+
downloadUrl?: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
48
62
|
export type NormalizedChannelMessage = {
|
|
49
63
|
role: ChannelMessageRole;
|
|
50
64
|
contentType: ChannelMessageContentType;
|
|
51
65
|
content: string;
|
|
52
66
|
providerMessageId?: string;
|
|
67
|
+
audio?: ChannelAudioAttachment;
|
|
53
68
|
};
|
|
54
69
|
|
|
55
70
|
export type NormalizedChannelEvent = ChannelAdapterId & {
|
|
@@ -87,6 +102,36 @@ export type ChannelSendInput = ChannelAdapterId & {
|
|
|
87
102
|
secrets: Record<string, string>;
|
|
88
103
|
};
|
|
89
104
|
|
|
105
|
+
export type ChannelMediaDownloadInput = ChannelAdapterId & {
|
|
106
|
+
channelId: string;
|
|
107
|
+
message: NormalizedChannelMessage;
|
|
108
|
+
secrets: Record<string, string>;
|
|
109
|
+
maxBytes?: number;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export type ChannelMediaDownloadResult =
|
|
113
|
+
| {
|
|
114
|
+
ok: true;
|
|
115
|
+
audio: Uint8Array;
|
|
116
|
+
filename: string;
|
|
117
|
+
mimeType?: string;
|
|
118
|
+
durationSeconds?: number;
|
|
119
|
+
sizeBytes?: number;
|
|
120
|
+
providerMetadata?: Record<string, unknown>;
|
|
121
|
+
}
|
|
122
|
+
| {
|
|
123
|
+
ok: false;
|
|
124
|
+
retryable: boolean;
|
|
125
|
+
code:
|
|
126
|
+
| "channel_secret_missing"
|
|
127
|
+
| "channel_audio_download_unavailable"
|
|
128
|
+
| "channel_audio_too_large"
|
|
129
|
+
| "channel_provider_unavailable"
|
|
130
|
+
| "channel_media_download_failed";
|
|
131
|
+
message: string;
|
|
132
|
+
providerMetadata?: Record<string, unknown>;
|
|
133
|
+
};
|
|
134
|
+
|
|
90
135
|
export type ChannelSendResult =
|
|
91
136
|
| {
|
|
92
137
|
ok: true;
|
|
@@ -120,6 +165,7 @@ export type ChannelStatusResult = {
|
|
|
120
165
|
export type ChannelAdapter = ChannelAdapterId & {
|
|
121
166
|
requiredSecrets: string[];
|
|
122
167
|
normalizeInbound(input: RawWebhookEvent): Promise<NormalizedChannelEvent[]> | NormalizedChannelEvent[];
|
|
168
|
+
downloadMedia?(input: ChannelMediaDownloadInput): Promise<ChannelMediaDownloadResult>;
|
|
123
169
|
sendMessage(input: ChannelSendInput): Promise<ChannelSendResult>;
|
|
124
170
|
verifyWebhook?(input: WebhookVerificationInput): Promise<WebhookVerificationResult> | WebhookVerificationResult;
|
|
125
171
|
getStatus?(input: ChannelStatusInput): Promise<ChannelStatusResult> | ChannelStatusResult;
|
package/src/runtime/chat.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { AgentKitError, isAgentKitError } from "./errors";
|
|
|
10
10
|
import { syncConfiguredKnowledgeSources } from "./knowledge/ingest";
|
|
11
11
|
import { appendKnowledgePromptPolicy } from "./knowledge/prompt-policy";
|
|
12
12
|
import { createKnowledgeSearchTool } from "./knowledge/tool";
|
|
13
|
+
import { appendRuntimePromptContext, buildRuntimeClockContext } from "./prompt-context";
|
|
13
14
|
import { createToolRuntime } from "./tools";
|
|
14
15
|
|
|
15
16
|
export type RunAgentMessageOptions = {
|
|
@@ -17,6 +18,7 @@ export type RunAgentMessageOptions = {
|
|
|
17
18
|
conversationId?: string;
|
|
18
19
|
signal?: AbortSignal;
|
|
19
20
|
runtime?: Partial<ToolRuntimeContext>;
|
|
21
|
+
now?: Date;
|
|
20
22
|
};
|
|
21
23
|
|
|
22
24
|
export type AgentRunResult = ProviderRunResult & PersistedChatRun;
|
|
@@ -41,51 +43,64 @@ export async function runAgentMessage(
|
|
|
41
43
|
const adapter = getProviderAdapter(capsule.config.provider);
|
|
42
44
|
const env = await loadCapsuleEnv(capsule.root);
|
|
43
45
|
await syncConfiguredKnowledgeSources(capsule);
|
|
44
|
-
const { store } = await openPreparedCapsuleStore(capsule);
|
|
45
46
|
const conversationId = normalizeConversationId(options.conversationId);
|
|
46
|
-
const {
|
|
47
|
-
|
|
48
|
-
conversationId,
|
|
49
|
-
firstMessage: content,
|
|
50
|
-
});
|
|
51
|
-
const messages: AgentMessage[] = [...previousMessages, { role: "user", content }];
|
|
52
|
-
const userMessage = store.appendMessage({
|
|
53
|
-
conversationId: conversation.id,
|
|
54
|
-
role: "user",
|
|
55
|
-
content,
|
|
56
|
-
});
|
|
57
|
-
const run = store.createRun({
|
|
58
|
-
conversationId: conversation.id,
|
|
59
|
-
provider: capsule.config.provider.name,
|
|
60
|
-
model: capsule.config.provider.model,
|
|
61
|
-
});
|
|
62
|
-
const runtimeTools = withKnowledgeTools(capsule);
|
|
63
|
-
const toolRuntime = createToolRuntime({
|
|
64
|
-
tools: runtimeTools,
|
|
65
|
-
store,
|
|
66
|
-
runId: run.id,
|
|
67
|
-
conversationId: conversation.id,
|
|
68
|
-
env,
|
|
69
|
-
signal: options.signal,
|
|
70
|
-
runtime: {
|
|
71
|
-
environment: "local",
|
|
72
|
-
invocation: "chat",
|
|
73
|
-
target: "bun",
|
|
74
|
-
database: "sqlite",
|
|
75
|
-
...options.runtime,
|
|
76
|
-
},
|
|
77
|
-
});
|
|
47
|
+
const { store } = await openPreparedCapsuleStore(capsule);
|
|
48
|
+
let runId: string | null = null;
|
|
78
49
|
|
|
79
50
|
try {
|
|
51
|
+
const { conversation, previousMessages } = resolveConversation(store, {
|
|
52
|
+
agentName: capsule.config.name,
|
|
53
|
+
conversationId,
|
|
54
|
+
firstMessage: content,
|
|
55
|
+
});
|
|
56
|
+
const messages: AgentMessage[] = [...previousMessages, { role: "user", content }];
|
|
57
|
+
const userMessage = store.appendMessage({
|
|
58
|
+
conversationId: conversation.id,
|
|
59
|
+
role: "user",
|
|
60
|
+
content,
|
|
61
|
+
});
|
|
62
|
+
const run = store.createRun({
|
|
63
|
+
conversationId: conversation.id,
|
|
64
|
+
provider: capsule.config.provider.name,
|
|
65
|
+
model: capsule.config.provider.model,
|
|
66
|
+
});
|
|
67
|
+
runId = run.id;
|
|
68
|
+
const runtimeTools = withKnowledgeTools(capsule);
|
|
69
|
+
const clock = buildRuntimeClockContext({
|
|
70
|
+
timeZone: capsule.config.timeZone,
|
|
71
|
+
env,
|
|
72
|
+
now: options.now,
|
|
73
|
+
});
|
|
74
|
+
const toolRuntime = createToolRuntime({
|
|
75
|
+
tools: runtimeTools,
|
|
76
|
+
store,
|
|
77
|
+
runId: run.id,
|
|
78
|
+
conversationId: conversation.id,
|
|
79
|
+
env,
|
|
80
|
+
signal: options.signal,
|
|
81
|
+
clock,
|
|
82
|
+
runtime: {
|
|
83
|
+
environment: "local",
|
|
84
|
+
invocation: "chat",
|
|
85
|
+
target: "bun",
|
|
86
|
+
database: "sqlite",
|
|
87
|
+
...options.runtime,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
80
90
|
const result = await adapter.run({
|
|
81
91
|
agent: {
|
|
82
92
|
name: capsule.config.name,
|
|
83
93
|
runtime: capsule.config.runtime,
|
|
84
94
|
provider: capsule.config.provider,
|
|
85
95
|
},
|
|
86
|
-
instructions:
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
instructions: appendRuntimePromptContext(
|
|
97
|
+
withToolVisibilityInstructions(
|
|
98
|
+
appendKnowledgePromptPolicy(capsule.instructions, capsule.config),
|
|
99
|
+
runtimeTools,
|
|
100
|
+
),
|
|
101
|
+
{
|
|
102
|
+
clock,
|
|
103
|
+
},
|
|
89
104
|
),
|
|
90
105
|
messages,
|
|
91
106
|
tools: toolRuntime.definitions,
|
|
@@ -114,12 +129,14 @@ export async function runAgentMessage(
|
|
|
114
129
|
assistantMessageId: assistantMessage.id,
|
|
115
130
|
};
|
|
116
131
|
} catch (error) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
132
|
+
if (runId) {
|
|
133
|
+
store.completeRun({
|
|
134
|
+
runId,
|
|
135
|
+
status: "failed",
|
|
136
|
+
errorCode: isAgentKitError(error) ? error.code : "runtime_error",
|
|
137
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
123
140
|
|
|
124
141
|
throw error;
|
|
125
142
|
} finally {
|