@tangle-network/agent-integrations 0.56.0 → 0.57.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.
@@ -203,6 +203,7 @@ __export(adapters_exports, {
203
203
  createLinkedinConnector: () => createLinkedinConnector,
204
204
  createMongoDbConnector: () => createMongoDbConnector,
205
205
  createMySqlConnector: () => createMySqlConnector,
206
+ createPhonyConnector: () => createPhonyConnector,
206
207
  createPostgresConnector: () => createPostgresConnector,
207
208
  createRabbitMqConnector: () => createRabbitMqConnector,
208
209
  createRedisConnector: () => createRedisConnector,
@@ -703,6 +704,575 @@ __export(adapters_exports, {
703
704
  zuoraConnector: () => zuoraConnector
704
705
  });
705
706
 
707
+ // src/connectors/adapters/phony.ts
708
+ import { VoiceClient, VoiceApiException } from "@ph0ny/sdk";
709
+ var E164 = /^\+[1-9]\d{7,14}$/;
710
+ function createPhonyConnector(options = {}) {
711
+ async function ph0ny(sourceId, token, request4, label) {
712
+ try {
713
+ return await new VoiceClient({ apiKey: token, baseUrl: options.baseUrl, fetchImpl: options.fetchImpl }).request(request4.method, request4.path, {
714
+ body: request4.body,
715
+ timeout: request4.timeout
716
+ });
717
+ } catch (error) {
718
+ if (!(error instanceof VoiceApiException)) throw error;
719
+ if (error.statusCode === 401) throw new CredentialsExpired("ph0ny rejected credentials (401)", sourceId);
720
+ throw new Error(`phony ${label} ${error.statusCode}: ${error.message.slice(0, 200)}`);
721
+ }
722
+ }
723
+ return {
724
+ manifest: {
725
+ kind: "phony",
726
+ displayName: "ph0ny",
727
+ description: "Place real outbound phone calls with a voice agent, then read back call status, transcript, and extracted fields. Outbound calls require recorded user consent and support a dry-run that validates the full configuration without dialing.",
728
+ auth: {
729
+ kind: "api-key",
730
+ hint: "Paste your ph0ny API key (plabs_\u2026). Create one in the developer portal via POST /v1/keys \u2014 it is shown once."
731
+ },
732
+ category: "comms",
733
+ // A call's status/transcript evolve while it is live, so a fetched row
734
+ // can be stale moments later — reads are point-in-time, not authoritative
735
+ // truth. start_outbound_call creates a fresh, uncontended call each time
736
+ // (cas='none', fire-and-forget external effect; no upstream CAS exists).
737
+ defaultConsistencyModel: "cache",
738
+ capabilities: [
739
+ {
740
+ name: "list_agents",
741
+ class: "read",
742
+ description: "List the voice agents on your ph0ny developer account (newest first).",
743
+ parameters: {
744
+ type: "object",
745
+ properties: {
746
+ limit: { type: "integer", minimum: 1, maximum: 100, default: 20 },
747
+ cursor: { type: "string", description: "Pagination cursor from a prior response." }
748
+ }
749
+ }
750
+ },
751
+ {
752
+ name: "get_call",
753
+ class: "read",
754
+ description: "Fetch a single outbound call by id, including status, transcript, and extracted fields.",
755
+ parameters: {
756
+ type: "object",
757
+ properties: {
758
+ id: { type: "string", description: "Outbound call id returned by start_outbound_call." }
759
+ },
760
+ required: ["id"]
761
+ }
762
+ },
763
+ {
764
+ name: "list_calls",
765
+ class: "read",
766
+ description: "List recent outbound calls for the account, optionally filtered by agent.",
767
+ parameters: {
768
+ type: "object",
769
+ properties: {
770
+ agentId: { type: "string", description: "Optional filter \u2014 only calls placed by this agent." },
771
+ limit: { type: "integer", minimum: 1, maximum: 50, default: 20 }
772
+ }
773
+ }
774
+ },
775
+ {
776
+ name: "start_outbound_call",
777
+ class: "mutation",
778
+ description: "Place an outbound phone call. Requires userConsentRecorded=true (the user must have explicitly authorized the call). Set dryRun=true to validate the full configuration without dialing.",
779
+ cas: "none",
780
+ externalEffect: true,
781
+ parameters: {
782
+ type: "object",
783
+ properties: {
784
+ agentId: { type: "string", description: "Agent that will place the call (agentKind must be personal_assistant)." },
785
+ toNumber: { type: "string", description: "E.164 destination, e.g. +14155551212." },
786
+ fromNumber: { type: "string", description: "E.164 caller number provisioned to your developer account." },
787
+ mission: {
788
+ type: "object",
789
+ description: "What the agent should accomplish on the call.",
790
+ properties: {
791
+ goal: { type: "string", description: "Plain-language objective (8\u20132000 chars)." },
792
+ successSchema: { type: "object", description: "Optional JSON schema describing fields to extract on success." },
793
+ ivrHints: {
794
+ type: "array",
795
+ items: { type: "string" },
796
+ description: "Optional hints for navigating phone-tree / IVR menus (\u22648 entries)."
797
+ },
798
+ maxTurns: { type: "integer", minimum: 1, maximum: 60 },
799
+ maxDurationMs: { type: "integer", minimum: 3e4, maximum: 12e5 }
800
+ },
801
+ required: ["goal"]
802
+ },
803
+ missionId: { type: "string", description: "Optional caller-supplied mission id." },
804
+ callerProfile: {
805
+ type: "object",
806
+ properties: {
807
+ userName: { type: "string" },
808
+ companyName: { type: "string" }
809
+ }
810
+ },
811
+ voiceCloneId: { type: "string", description: "Optional cloned-voice id to speak with." },
812
+ userConsentRecorded: {
813
+ type: "boolean",
814
+ description: "REQUIRED. Must be true \u2014 the user explicitly authorized this call. ph0ny rejects false."
815
+ },
816
+ dryRun: {
817
+ type: "boolean",
818
+ description: "When true, validate every gate and return a dryRunReport without placing the call."
819
+ }
820
+ },
821
+ required: ["agentId", "toNumber", "fromNumber", "mission", "userConsentRecorded"]
822
+ }
823
+ },
824
+ {
825
+ name: "create_agent",
826
+ class: "mutation",
827
+ description: "Create a voice agent on your ph0ny developer account. Only name is required; the rest configure the LLM, TTS, voice, knowledge base, and capture behavior.",
828
+ cas: "none",
829
+ externalEffect: true,
830
+ parameters: {
831
+ type: "object",
832
+ properties: {
833
+ id: { type: "string", description: "Optional caller-supplied agent id (1\u201360 chars)." },
834
+ name: { type: "string", description: "Display name (1\u2013100 chars)." },
835
+ description: { type: "string", description: "Optional description (\u22641000 chars)." },
836
+ systemPrompt: { type: "string", description: "Optional system prompt (\u226410000 chars)." },
837
+ firstMessage: { type: "string", description: "Optional opening line the agent speaks first (\u22641000 chars)." },
838
+ voiceId: { type: "string", description: "Optional TTS voice id." },
839
+ ttsProvider: { type: "string", description: 'TTS provider; "default" lets ph0ny choose.' },
840
+ ttsModel: { type: "string", description: "Optional TTS model id." },
841
+ sttProvider: { type: "string", description: "Optional speech-to-text provider." },
842
+ llmProvider: { type: "string", description: "LLM provider (e.g. openai, anthropic). Defaults to openai server-side." },
843
+ llmModel: { type: "string", description: "Optional LLM model id." },
844
+ libraryId: { type: "string", description: "Ingest-service library id (preferred over collectionId)." },
845
+ collectionId: { type: "string", description: "Deprecated \u2014 KB collection id to attach. Prefer libraryId." },
846
+ styleProfile: { type: "object", description: "Optional style profile object." },
847
+ promptByModel: { type: "object", description: "Optional per-model prompt overrides (modelId \u2192 prompt)." },
848
+ language: { type: "string", description: 'BCP-47 language code; defaults to "en".' },
849
+ temperature: { type: "number", minimum: 0, maximum: 2, description: "Sampling temperature (0\u20132); defaults to 0.7." },
850
+ maxTokens: { type: "integer", minimum: 1, maximum: 16384, description: "Optional max output tokens." },
851
+ contactCaptureEnabled: { type: "boolean", description: "Enable structured contact capture during calls." },
852
+ contactCaptureFields: {
853
+ type: "array",
854
+ items: { type: "string", enum: ["name", "email", "phone"] },
855
+ description: "Which contact fields to capture when capture is enabled."
856
+ },
857
+ metadata: { type: "object", description: "Arbitrary metadata object stored with the agent." }
858
+ },
859
+ required: ["name"]
860
+ }
861
+ },
862
+ {
863
+ name: "provision_agent",
864
+ class: "mutation",
865
+ description: "Create an agent and, in the same atomic call, optionally a fresh knowledge-base collection and seed it with initial content. Returns the agent plus the created collection and ingestion results.",
866
+ cas: "none",
867
+ externalEffect: true,
868
+ parameters: {
869
+ type: "object",
870
+ properties: {
871
+ id: { type: "string", description: "Optional caller-supplied agent id (1\u201360 chars)." },
872
+ name: { type: "string", description: "Display name (1\u2013100 chars)." },
873
+ description: { type: "string", description: "Optional description (\u22641000 chars)." },
874
+ systemPrompt: { type: "string", description: "Optional system prompt (\u226410000 chars)." },
875
+ firstMessage: { type: "string", description: "Optional opening line the agent speaks first (\u22641000 chars)." },
876
+ voiceId: { type: "string", description: "Optional TTS voice id." },
877
+ ttsProvider: { type: "string", description: 'TTS provider; "default" lets ph0ny choose.' },
878
+ ttsModel: { type: "string", description: "Optional TTS model id." },
879
+ sttProvider: { type: "string", description: "Optional speech-to-text provider." },
880
+ llmProvider: { type: "string", description: "LLM provider (e.g. openai, anthropic). Defaults to openai server-side." },
881
+ llmModel: { type: "string", description: "Optional LLM model id." },
882
+ libraryId: { type: "string", description: "Ingest-service library id (preferred over collectionId)." },
883
+ collectionId: { type: "string", description: "Existing KB collection id to attach instead of creating one." },
884
+ styleProfile: { type: "object", description: "Optional style profile object." },
885
+ promptByModel: { type: "object", description: "Optional per-model prompt overrides (modelId \u2192 prompt)." },
886
+ language: { type: "string", description: 'BCP-47 language code; defaults to "en".' },
887
+ temperature: { type: "number", minimum: 0, maximum: 2, description: "Sampling temperature (0\u20132); defaults to 0.7." },
888
+ maxTokens: { type: "integer", minimum: 1, maximum: 16384, description: "Optional max output tokens." },
889
+ contactCaptureEnabled: { type: "boolean", description: "Enable structured contact capture during calls." },
890
+ contactCaptureFields: {
891
+ type: "array",
892
+ items: { type: "string", enum: ["name", "email", "phone"] },
893
+ description: "Which contact fields to capture when capture is enabled."
894
+ },
895
+ metadata: { type: "object", description: "Arbitrary metadata object stored with the agent." },
896
+ collection: {
897
+ type: "object",
898
+ description: "When present, create a fresh KB collection and attach it to the agent.",
899
+ properties: {
900
+ name: { type: "string", description: "Collection name (1\u2013100 chars)." },
901
+ description: { type: "string", description: "Optional collection description (\u2264500 chars)." }
902
+ },
903
+ required: ["name"]
904
+ },
905
+ initialContent: {
906
+ type: "array",
907
+ maxItems: 10,
908
+ description: "Documents to ingest into the new/attached collection (\u226410).",
909
+ items: {
910
+ type: "object",
911
+ properties: {
912
+ content: { type: "string", description: "Raw text or transcript content." },
913
+ contentType: { type: "string", enum: ["text", "transcript"], description: 'Defaults to "text".' },
914
+ metadata: { type: "object", description: "Arbitrary metadata stored with the document." }
915
+ },
916
+ required: ["content"]
917
+ }
918
+ }
919
+ },
920
+ required: ["name"]
921
+ }
922
+ },
923
+ {
924
+ name: "kb_create_collection",
925
+ class: "mutation",
926
+ description: "Create a knowledge-base collection the agent can search at call time.",
927
+ cas: "none",
928
+ externalEffect: true,
929
+ parameters: {
930
+ type: "object",
931
+ properties: {
932
+ name: { type: "string", description: "Collection name (1\u2013100 chars)." },
933
+ description: { type: "string", description: "Optional description (\u2264500 chars)." },
934
+ metadata: { type: "object", description: "Arbitrary metadata object stored with the collection." }
935
+ },
936
+ required: ["name"]
937
+ }
938
+ },
939
+ {
940
+ name: "kb_ingest",
941
+ class: "mutation",
942
+ description: 'Chunk, embed, and store content into a KB collection. Provide content directly (\u22641MB), or a sourceUrl for audio transcription. contentType "url" is not yet implemented server-side (501).',
943
+ cas: "none",
944
+ externalEffect: true,
945
+ parameters: {
946
+ type: "object",
947
+ properties: {
948
+ collectionId: { type: "string", description: "Target collection id." },
949
+ content: { type: "string", description: "Raw text/transcript content (\u22641,000,000 chars)." },
950
+ sourceUrl: { type: "string", description: 'Public URL \u2014 required when contentType is "audio" (transcribed) or "url".' },
951
+ contentType: {
952
+ type: "string",
953
+ enum: ["text", "transcript", "audio", "url"],
954
+ description: 'Defaults to "text". "audio" transcribes sourceUrl; "url" returns 501.'
955
+ },
956
+ metadata: { type: "object", description: "Arbitrary metadata stored with the document." },
957
+ chunkSize: { type: "integer", minimum: 100, maximum: 4e3, description: "Chunk size in chars; defaults to 1000." },
958
+ chunkOverlap: { type: "integer", minimum: 0, maximum: 500, description: "Chunk overlap in chars; defaults to 200. Must be < chunkSize." }
959
+ },
960
+ required: ["collectionId"]
961
+ }
962
+ },
963
+ {
964
+ name: "kb_search",
965
+ class: "read",
966
+ description: "Hybrid vector + keyword search over a KB collection. Returns ranked results with scores.",
967
+ parameters: {
968
+ type: "object",
969
+ properties: {
970
+ collectionId: { type: "string", description: "Collection id to search." },
971
+ query: { type: "string", description: "Search query (1\u20132000 chars)." },
972
+ limit: { type: "integer", minimum: 1, maximum: 100, description: "Max results; defaults to 10." },
973
+ threshold: { type: "number", minimum: 0, maximum: 1, description: "Minimum similarity score; defaults to 0.7." },
974
+ includeMetadata: { type: "boolean", description: "Include per-result metadata; defaults to true." }
975
+ },
976
+ required: ["collectionId", "query"]
977
+ }
978
+ }
979
+ ]
980
+ },
981
+ async executeRead(inv) {
982
+ const token = bearerToken(inv.source.credentials);
983
+ if (inv.capabilityName === "list_agents") {
984
+ const { limit, cursor } = inv.args;
985
+ const params = new URLSearchParams();
986
+ params.set("limit", String(Math.min(Math.max(1, limit ?? 20), 100)));
987
+ if (cursor) params.set("cursor", cursor);
988
+ const json = await ph0ny(
989
+ inv.source.id,
990
+ token,
991
+ { method: "GET", path: `/v1/agents?${params.toString()}`, timeout: 1e4 },
992
+ "list_agents"
993
+ );
994
+ return {
995
+ data: { agents: json.data ?? [], nextCursor: json.nextCursor ?? null, hasMore: json.hasMore ?? false },
996
+ fetchedAt: Date.now()
997
+ };
998
+ }
999
+ if (inv.capabilityName === "get_call") {
1000
+ const { id: id7 } = inv.args;
1001
+ const json = await ph0ny(
1002
+ inv.source.id,
1003
+ token,
1004
+ { method: "GET", path: `/v1/outbound/${encodeURIComponent(id7)}`, timeout: 1e4 },
1005
+ "get_call"
1006
+ );
1007
+ return { data: { call: json.call ?? null }, fetchedAt: Date.now() };
1008
+ }
1009
+ if (inv.capabilityName === "list_calls") {
1010
+ const { agentId, limit } = inv.args;
1011
+ const params = new URLSearchParams();
1012
+ params.set("limit", String(Math.min(Math.max(1, limit ?? 20), 50)));
1013
+ if (agentId) params.set("agentId", agentId);
1014
+ const json = await ph0ny(
1015
+ inv.source.id,
1016
+ token,
1017
+ { method: "GET", path: `/v1/outbound?${params.toString()}`, timeout: 1e4 },
1018
+ "list_calls"
1019
+ );
1020
+ return { data: { calls: json.calls ?? [] }, fetchedAt: Date.now() };
1021
+ }
1022
+ if (inv.capabilityName === "kb_search") {
1023
+ const { collectionId, query: query2, limit, threshold, includeMetadata } = inv.args;
1024
+ const payload = { query: query2 };
1025
+ if (limit !== void 0) payload.limit = limit;
1026
+ if (threshold !== void 0) payload.threshold = threshold;
1027
+ if (includeMetadata !== void 0) payload.includeMetadata = includeMetadata;
1028
+ const json = await ph0ny(
1029
+ inv.source.id,
1030
+ token,
1031
+ {
1032
+ method: "POST",
1033
+ path: `/v1/collections/${encodeURIComponent(collectionId)}/search`,
1034
+ body: payload,
1035
+ timeout: 15e3
1036
+ },
1037
+ "kb_search"
1038
+ );
1039
+ return {
1040
+ data: {
1041
+ results: json.results ?? [],
1042
+ queryTokens: json.queryTokens ?? 0,
1043
+ ...json.graphContext !== void 0 ? { graphContext: json.graphContext } : {}
1044
+ },
1045
+ fetchedAt: Date.now()
1046
+ };
1047
+ }
1048
+ throw new Error(`phony: unknown read capability ${inv.capabilityName}`);
1049
+ },
1050
+ async executeMutation(inv) {
1051
+ const token = bearerToken(inv.source.credentials);
1052
+ if (inv.capabilityName === "start_outbound_call") {
1053
+ const args = validateOutboundStartArgs(inv.args);
1054
+ const payload = {
1055
+ agentId: args.agentId,
1056
+ toNumber: args.toNumber,
1057
+ fromNumber: args.fromNumber,
1058
+ mission: args.mission,
1059
+ userConsentRecorded: args.userConsentRecorded
1060
+ };
1061
+ if (args.missionId !== void 0) payload.missionId = args.missionId;
1062
+ if (args.callerProfile !== void 0) payload.callerProfile = args.callerProfile;
1063
+ if (args.voiceCloneId !== void 0) payload.voiceCloneId = args.voiceCloneId;
1064
+ if (args.dryRun !== void 0) payload.dryRun = args.dryRun;
1065
+ const json = await ph0ny(inv.source.id, token, { method: "POST", path: "/v1/outbound/start", body: payload, timeout: 2e4 }, "start_outbound_call");
1066
+ return {
1067
+ status: "committed",
1068
+ data: {
1069
+ callId: json.callId,
1070
+ callSid: json.callSid,
1071
+ callStatus: json.status,
1072
+ dryRun: json.dryRun ?? false,
1073
+ ...json.dryRunReport !== void 0 ? { dryRunReport: json.dryRunReport } : {}
1074
+ },
1075
+ committedAt: Date.now(),
1076
+ idempotentReplay: false
1077
+ };
1078
+ }
1079
+ if (inv.capabilityName === "create_agent") {
1080
+ const payload = pick(inv.args, AGENT_FIELDS);
1081
+ const json = await ph0ny(
1082
+ inv.source.id,
1083
+ token,
1084
+ { method: "POST", path: `/v1/agents`, body: payload, timeout: 2e4 },
1085
+ "create_agent"
1086
+ );
1087
+ return {
1088
+ status: "committed",
1089
+ data: { agent: json },
1090
+ committedAt: Date.now(),
1091
+ idempotentReplay: false
1092
+ };
1093
+ }
1094
+ if (inv.capabilityName === "provision_agent") {
1095
+ const args = inv.args;
1096
+ const payload = pick(args, AGENT_FIELDS);
1097
+ if (args.collection !== void 0) payload.collection = args.collection;
1098
+ if (args.initialContent !== void 0) payload.initialContent = args.initialContent;
1099
+ const json = await ph0ny(
1100
+ inv.source.id,
1101
+ token,
1102
+ { method: "POST", path: `/v1/agents/provision`, body: payload, timeout: 2e4 },
1103
+ "provision_agent"
1104
+ );
1105
+ return {
1106
+ status: "committed",
1107
+ data: {
1108
+ agent: json.agent ?? null,
1109
+ ...json.collection !== void 0 ? { collection: json.collection } : {},
1110
+ ...json.ingested !== void 0 ? { ingested: json.ingested } : {}
1111
+ },
1112
+ committedAt: Date.now(),
1113
+ idempotentReplay: false
1114
+ };
1115
+ }
1116
+ if (inv.capabilityName === "kb_create_collection") {
1117
+ const { name, description, metadata } = inv.args;
1118
+ const payload = { name };
1119
+ if (description !== void 0) payload.description = description;
1120
+ if (metadata !== void 0) payload.metadata = metadata;
1121
+ const json = await ph0ny(
1122
+ inv.source.id,
1123
+ token,
1124
+ { method: "POST", path: `/v1/collections`, body: payload, timeout: 2e4 },
1125
+ "kb_create_collection"
1126
+ );
1127
+ return {
1128
+ status: "committed",
1129
+ data: { collection: json },
1130
+ committedAt: Date.now(),
1131
+ idempotentReplay: false
1132
+ };
1133
+ }
1134
+ if (inv.capabilityName === "kb_ingest") {
1135
+ const { collectionId, ...rest } = inv.args;
1136
+ const payload = pick(rest, INGEST_FIELDS);
1137
+ const json = await ph0ny(
1138
+ inv.source.id,
1139
+ token,
1140
+ { method: "POST", path: `/v1/collections/${encodeURIComponent(collectionId)}/ingest`, body: payload, timeout: 2e4 },
1141
+ "kb_ingest"
1142
+ );
1143
+ return {
1144
+ status: "committed",
1145
+ data: {
1146
+ documentId: json.documentId ?? null,
1147
+ chunksCreated: json.chunksCreated ?? 0,
1148
+ tokensUsed: json.tokensUsed ?? 0
1149
+ },
1150
+ committedAt: Date.now(),
1151
+ idempotentReplay: false
1152
+ };
1153
+ }
1154
+ throw new Error(`phony: unknown mutation capability ${inv.capabilityName}`);
1155
+ },
1156
+ async test(source) {
1157
+ try {
1158
+ const token = bearerToken(source.credentials);
1159
+ await new VoiceClient({ apiKey: token, baseUrl: options.baseUrl, fetchImpl: options.fetchImpl }).request("GET", "/v1/outbound?limit=1", { timeout: 8e3 });
1160
+ return { ok: true };
1161
+ } catch (err) {
1162
+ if (err instanceof VoiceApiException) {
1163
+ return {
1164
+ ok: false,
1165
+ reason: err.statusCode === 401 ? "ph0ny rejected credentials (401) \u2014 reconnect required" : `ph0ny returned ${err.statusCode}`
1166
+ };
1167
+ }
1168
+ return { ok: false, reason: err instanceof Error ? err.message : String(err) };
1169
+ }
1170
+ }
1171
+ };
1172
+ }
1173
+ var phonyConnector = createPhonyConnector();
1174
+ function bearerToken(creds) {
1175
+ if (creds.kind !== "api-key" || typeof creds.apiKey !== "string" || creds.apiKey.length === 0) {
1176
+ throw new Error("phony: expected api-key credentials");
1177
+ }
1178
+ return creds.apiKey;
1179
+ }
1180
+ function validateOutboundStartArgs(args) {
1181
+ if (args.userConsentRecorded !== true) {
1182
+ throw new Error(
1183
+ "phony start_outbound_call requires userConsentRecorded=true after explicit user authorization; refusing before contacting ph0ny"
1184
+ );
1185
+ }
1186
+ assertNonEmptyString(args.agentId, "agentId", 128);
1187
+ assertE164(args.toNumber, "toNumber");
1188
+ assertE164(args.fromNumber, "fromNumber");
1189
+ if (!isRecord(args.mission)) throw new Error("phony start_outbound_call requires mission");
1190
+ const mission = args.mission;
1191
+ assertNonEmptyString(mission.goal, "mission.goal", 2e3);
1192
+ if (mission.goal.trim().length < 8) throw new Error("phony start_outbound_call mission.goal must be at least 8 characters");
1193
+ if (mission.successSchema !== void 0 && !isRecord(mission.successSchema)) {
1194
+ throw new Error("phony start_outbound_call mission.successSchema must be an object when supplied");
1195
+ }
1196
+ if (mission.ivrHints !== void 0) {
1197
+ if (!Array.isArray(mission.ivrHints) || mission.ivrHints.length > 8) {
1198
+ throw new Error("phony start_outbound_call mission.ivrHints must be an array with at most 8 entries");
1199
+ }
1200
+ for (const hint of mission.ivrHints) assertNonEmptyString(hint, "mission.ivrHints[]", 200);
1201
+ }
1202
+ assertOptionalInteger(mission.maxTurns, "mission.maxTurns", 1, 60);
1203
+ assertOptionalInteger(mission.maxDurationMs, "mission.maxDurationMs", 3e4, 20 * 60 * 1e3);
1204
+ if (args.missionId !== void 0) assertNonEmptyString(args.missionId, "missionId", 128);
1205
+ if (args.voiceCloneId !== void 0) assertNonEmptyString(args.voiceCloneId, "voiceCloneId", 128);
1206
+ if (args.callerProfile !== void 0) {
1207
+ if (!isRecord(args.callerProfile)) throw new Error("phony start_outbound_call callerProfile must be an object");
1208
+ if (args.callerProfile.userName !== void 0) assertNonEmptyString(args.callerProfile.userName, "callerProfile.userName", 120);
1209
+ if (args.callerProfile.companyName !== void 0) {
1210
+ assertNonEmptyString(args.callerProfile.companyName, "callerProfile.companyName", 120);
1211
+ }
1212
+ }
1213
+ if (args.dryRun !== void 0 && typeof args.dryRun !== "boolean") {
1214
+ throw new Error("phony start_outbound_call dryRun must be boolean when supplied");
1215
+ }
1216
+ return args;
1217
+ }
1218
+ function assertNonEmptyString(value, field, maxLength) {
1219
+ if (typeof value !== "string" || value.trim().length === 0 || value.length > maxLength) {
1220
+ throw new Error(`phony start_outbound_call ${field} must be a non-empty string <= ${maxLength} chars`);
1221
+ }
1222
+ }
1223
+ function assertE164(value, field) {
1224
+ if (typeof value !== "string" || !E164.test(value)) {
1225
+ throw new Error(`phony start_outbound_call ${field} must be E.164 format, e.g. +14155550123`);
1226
+ }
1227
+ }
1228
+ function assertOptionalInteger(value, field, min, max) {
1229
+ if (value === void 0) return;
1230
+ if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
1231
+ throw new Error(`phony start_outbound_call ${field} must be an integer between ${min} and ${max}`);
1232
+ }
1233
+ }
1234
+ function isRecord(value) {
1235
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1236
+ }
1237
+ function pick(args, fields) {
1238
+ const out = {};
1239
+ for (const key of fields) {
1240
+ if (args[key] !== void 0) out[key] = args[key];
1241
+ }
1242
+ return out;
1243
+ }
1244
+ var AGENT_FIELDS = [
1245
+ "id",
1246
+ "name",
1247
+ "description",
1248
+ "systemPrompt",
1249
+ "firstMessage",
1250
+ "voiceId",
1251
+ "ttsProvider",
1252
+ "ttsModel",
1253
+ "sttProvider",
1254
+ "llmProvider",
1255
+ "llmModel",
1256
+ "libraryId",
1257
+ "collectionId",
1258
+ "styleProfile",
1259
+ "promptByModel",
1260
+ "language",
1261
+ "temperature",
1262
+ "maxTokens",
1263
+ "contactCaptureEnabled",
1264
+ "contactCaptureFields",
1265
+ "metadata"
1266
+ ];
1267
+ var INGEST_FIELDS = [
1268
+ "content",
1269
+ "sourceUrl",
1270
+ "contentType",
1271
+ "metadata",
1272
+ "chunkSize",
1273
+ "chunkOverlap"
1274
+ ];
1275
+
706
1276
  // src/connectors/adapters/declarative-rest.ts
707
1277
  import { isIP } from "net";
708
1278
 
@@ -8956,7 +9526,7 @@ var modelslabConnector = {
8956
9526
  }
8957
9527
  const prompt = requiredString2(inv.args, "prompt");
8958
9528
  const data4 = await request(inv.source, `${baseUrl}/text2img`, {
8959
- ...pick(inv.args, [
9529
+ ...pick2(inv.args, [
8960
9530
  "negative_prompt",
8961
9531
  "model_id",
8962
9532
  "width",
@@ -9027,7 +9597,7 @@ function requiredString2(args, key) {
9027
9597
  if (typeof value !== "string" || !value) throw new Error(`modelslab: ${key} is required`);
9028
9598
  return value;
9029
9599
  }
9030
- function pick(args, keys) {
9600
+ function pick2(args, keys) {
9031
9601
  return Object.fromEntries(keys.filter((key) => args[key] !== void 0).map((key) => [key, args[key]]));
9032
9602
  }
9033
9603
  async function readBody(response) {
@@ -25216,7 +25786,7 @@ var azureEventGridConnector = {
25216
25786
  const entries = Array.isArray(parsed) ? parsed : [parsed];
25217
25787
  if (entries.length > MAX_EVENTS) throw new Error(`Azure Event Grid delivery exceeds ${MAX_EVENTS} events`);
25218
25788
  const validation = entries.find((entry) => isValidationEvent(entry));
25219
- if (validation && isRecord(validation.data) && typeof validation.data.validationCode === "string") {
25789
+ if (validation && isRecord2(validation.data) && typeof validation.data.validationCode === "string") {
25220
25790
  return {
25221
25791
  events: [],
25222
25792
  response: { status: 200, body: { validationResponse: validation.data.validationCode } }
@@ -25266,7 +25836,7 @@ function parseCredentials(credentials, dataSourceId) {
25266
25836
  } catch {
25267
25837
  throw new CredentialsExpired("Azure Event Grid credentials must be valid JSON", dataSourceId);
25268
25838
  }
25269
- if (!isRecord(parsed)) throw new CredentialsExpired("Azure Event Grid credentials must be a JSON object", dataSourceId);
25839
+ if (!isRecord2(parsed)) throw new CredentialsExpired("Azure Event Grid credentials must be a JSON object", dataSourceId);
25270
25840
  const endpointValue = credentialString(parsed.endpoint, "endpoint", dataSourceId);
25271
25841
  const accessKey = credentialString(parsed.accessKey, "accessKey", dataSourceId);
25272
25842
  const deliverySecret = credentialString(parsed.deliverySecret, "deliverySecret", dataSourceId);
@@ -25331,12 +25901,12 @@ function eventArray(value) {
25331
25901
  throw new Error(`events must contain 1 to ${MAX_EVENTS} entries`);
25332
25902
  }
25333
25903
  return value.map((entry, index) => {
25334
- if (!isRecord(entry)) throw new Error(`events[${index}] must be an object`);
25904
+ if (!isRecord2(entry)) throw new Error(`events[${index}] must be an object`);
25335
25905
  return entry;
25336
25906
  });
25337
25907
  }
25338
25908
  function inboundEvent(entry, index) {
25339
- if (!isRecord(entry)) throw new Error(`Azure Event Grid delivery entry ${index} must be an object`);
25909
+ if (!isRecord2(entry)) throw new Error(`Azure Event Grid delivery entry ${index} must be an object`);
25340
25910
  const eventType = typeof entry.eventType === "string" ? entry.eventType : typeof entry.type === "string" ? entry.type : "azure.event-grid.event";
25341
25911
  return {
25342
25912
  eventType,
@@ -25345,7 +25915,7 @@ function inboundEvent(entry, index) {
25345
25915
  };
25346
25916
  }
25347
25917
  function isValidationEvent(value) {
25348
- return isRecord(value) && value.eventType === "Microsoft.EventGrid.SubscriptionValidationEvent";
25918
+ return isRecord2(value) && value.eventType === "Microsoft.EventGrid.SubscriptionValidationEvent";
25349
25919
  }
25350
25920
  function throwEventGridError(response, body, inv) {
25351
25921
  const redacted = body.replace(/[A-Za-z0-9+/]{32,}={0,2}/g, "[redacted]").slice(0, 500);
@@ -25436,7 +26006,7 @@ function retryAfterMs4(value) {
25436
26006
  const date4 = Date.parse(value);
25437
26007
  return Number.isFinite(date4) ? Math.max(0, date4 - Date.now()) : 6e4;
25438
26008
  }
25439
- function isRecord(value) {
26009
+ function isRecord2(value) {
25440
26010
  return typeof value === "object" && value !== null && !Array.isArray(value);
25441
26011
  }
25442
26012
 
@@ -25842,7 +26412,7 @@ var BROKER_PROPERTY_NAMES = /* @__PURE__ */ new Set([
25842
26412
  ]);
25843
26413
  function allowedBrokerProperties(value) {
25844
26414
  if (value === void 0) return void 0;
25845
- if (!isRecord2(value)) throw new Error("brokerProperties must be an object");
26415
+ if (!isRecord3(value)) throw new Error("brokerProperties must be an object");
25846
26416
  const entries = Object.entries(value);
25847
26417
  if (entries.length > BROKER_PROPERTY_NAMES.size) throw new Error("brokerProperties contains too many fields");
25848
26418
  const result = {};
@@ -25865,7 +26435,7 @@ var RESERVED_HEADERS = /* @__PURE__ */ new Set([
25865
26435
  ]);
25866
26436
  function applicationPropertyHeaders(value) {
25867
26437
  if (value === void 0) return {};
25868
- if (!isRecord2(value)) throw new Error("applicationProperties must be an object");
26438
+ if (!isRecord3(value)) throw new Error("applicationProperties must be an object");
25869
26439
  const entries = Object.entries(value);
25870
26440
  if (entries.length > 64) throw new Error("applicationProperties cannot contain more than 64 entries");
25871
26441
  const result = {};
@@ -25997,7 +26567,7 @@ function retryAfterMs5(value) {
25997
26567
  function redactProviderText(value) {
25998
26568
  return value.replace(/SharedAccessSignature\s+[^\s<]+/gi, "SharedAccessSignature [redacted]").replace(/SharedAccessKey=[^;\s<]+/gi, "SharedAccessKey=[redacted]");
25999
26569
  }
26000
- function isRecord2(value) {
26570
+ function isRecord3(value) {
26001
26571
  return typeof value === "object" && value !== null && !Array.isArray(value);
26002
26572
  }
26003
26573
 
@@ -40326,7 +40896,7 @@ function parseServiceAccountCredentials(credentials, dataSourceId) {
40326
40896
  } catch {
40327
40897
  throw new CredentialsExpired("Google Pub/Sub service-account key must be valid JSON", dataSourceId);
40328
40898
  }
40329
- if (!isRecord3(parsed) || parsed.type !== "service_account") {
40899
+ if (!isRecord4(parsed) || parsed.type !== "service_account") {
40330
40900
  throw new CredentialsExpired("Google Pub/Sub requires a Google service_account key", dataSourceId);
40331
40901
  }
40332
40902
  const projectId = requiredCredentialString(parsed.project_id, "project_id", dataSourceId);
@@ -40386,8 +40956,8 @@ async function exchangeAccessToken(credentials, dataSourceId, cacheKey) {
40386
40956
  body: form2
40387
40957
  });
40388
40958
  const body = await readJson(response, 64 * 1024);
40389
- if (!response.ok || !isRecord3(body) || typeof body.access_token !== "string" || body.access_token.trim() === "") {
40390
- const reason = isRecord3(body) && typeof body.error === "string" ? body.error : void 0;
40959
+ if (!response.ok || !isRecord4(body) || typeof body.access_token !== "string" || body.access_token.trim() === "") {
40960
+ const reason = isRecord4(body) && typeof body.error === "string" ? body.error : void 0;
40391
40961
  throw new CredentialsExpired("Google rejected the Pub/Sub service-account grant", dataSourceId, {
40392
40962
  status: response.status,
40393
40963
  reason,
@@ -40425,7 +40995,7 @@ async function pubSubRequest(credentials, inv, method, path, query2, body) {
40425
40995
  });
40426
40996
  const responseBody = await readJson(response, MAX_RESPONSE_BYTES4);
40427
40997
  if (response.ok) return responseBody;
40428
- const providerError = isRecord3(responseBody) && isRecord3(responseBody.error) ? responseBody.error : void 0;
40998
+ const providerError = isRecord4(responseBody) && isRecord4(responseBody.error) ? responseBody.error : void 0;
40429
40999
  const reason = providerError && typeof providerError.status === "string" ? providerError.status : void 0;
40430
41000
  if (response.status === 401) {
40431
41001
  throw new CredentialsExpired("Google Pub/Sub credentials were rejected", inv.source.id, {
@@ -40506,7 +41076,7 @@ function publishMessages(value) {
40506
41076
  throw new Error("messages must contain 1 to 1000 entries");
40507
41077
  }
40508
41078
  return value.map((entry, index) => {
40509
- if (!isRecord3(entry)) throw new Error(`messages[${index}] must be an object`);
41079
+ if (!isRecord4(entry)) throw new Error(`messages[${index}] must be an object`);
40510
41080
  const hasText = typeof entry.text === "string";
40511
41081
  const hasData = typeof entry.data === "string";
40512
41082
  if (hasText === hasData) throw new Error(`messages[${index}] requires exactly one of text or data`);
@@ -40577,7 +41147,7 @@ function optionalStringArray(value, field) {
40577
41147
  }
40578
41148
  function optionalStringMap(value, field) {
40579
41149
  if (value === void 0) return void 0;
40580
- if (!isRecord3(value)) throw new Error(`${field} must be an object of strings`);
41150
+ if (!isRecord4(value)) throw new Error(`${field} must be an object of strings`);
40581
41151
  const entries = Object.entries(value);
40582
41152
  if (entries.length > 64) throw new Error(`${field} cannot contain more than 64 entries`);
40583
41153
  return Object.fromEntries(entries.map(([key, entry]) => [
@@ -40623,7 +41193,7 @@ function retryAfterMs6(value) {
40623
41193
  const date4 = Date.parse(value);
40624
41194
  return Number.isFinite(date4) ? Math.max(0, date4 - Date.now()) : 6e4;
40625
41195
  }
40626
- function isRecord3(value) {
41196
+ function isRecord4(value) {
40627
41197
  return typeof value === "object" && value !== null && !Array.isArray(value);
40628
41198
  }
40629
41199
 
@@ -74479,7 +75049,7 @@ var zohoBookingsConnector = {
74479
75049
  break;
74480
75050
  case "appointments.list": {
74481
75051
  requireString(inv.args, "from_time");
74482
- const filters = pick2(inv.args, ["from_time", "to_time", "service_id", "staff_id", "status"]);
75052
+ const filters = pick3(inv.args, ["from_time", "to_time", "service_id", "staff_id", "status"]);
74483
75053
  data4 = await request2(inv.source, "/fetchappointment", {
74484
75054
  method: "POST",
74485
75055
  body: form({ data: JSON.stringify(filters) })
@@ -74514,7 +75084,7 @@ var zohoBookingsConnector = {
74514
75084
  for (const key of ["service_id", "from_time"]) requireString(inv.args, key);
74515
75085
  const customer = requireObject(inv.args, "customer_details");
74516
75086
  exactlyOne(inv.args, ["staff_id", "resource_id", "group_id"], "booking assignment");
74517
- const fields = pick2(inv.args, [
75087
+ const fields = pick3(inv.args, [
74518
75088
  "service_id",
74519
75089
  "from_time",
74520
75090
  "staff_id",
@@ -74536,7 +75106,7 @@ var zohoBookingsConnector = {
74536
75106
  exactlyOne(inv.args, ["start_time", "staff_id", "group_id"], "reschedule target");
74537
75107
  data4 = await request2(inv.source, "/rescheduleappointment", {
74538
75108
  method: "POST",
74539
- body: form(pick2(inv.args, ["booking_id", "start_time", "staff_id", "group_id"]))
75109
+ body: form(pick3(inv.args, ["booking_id", "start_time", "staff_id", "group_id"]))
74540
75110
  });
74541
75111
  break;
74542
75112
  }
@@ -74646,7 +75216,7 @@ async function readBody2(response) {
74646
75216
  }
74647
75217
  function query(args, keys, required = []) {
74648
75218
  for (const key of required) requireString(args, key);
74649
- return form(pick2(args, keys));
75219
+ return form(pick3(args, keys));
74650
75220
  }
74651
75221
  function form(values) {
74652
75222
  const result = new URLSearchParams();
@@ -74655,7 +75225,7 @@ function form(values) {
74655
75225
  }
74656
75226
  return result;
74657
75227
  }
74658
- function pick2(args, keys) {
75228
+ function pick3(args, keys) {
74659
75229
  return Object.fromEntries(keys.filter((key) => args[key] !== void 0).map((key) => [key, args[key]]));
74660
75230
  }
74661
75231
  function requireString(args, key) {
@@ -84204,6 +84774,8 @@ var biginByZohoOAuthEnvMap = {
84204
84774
  clientSecret: "BIGIN_BY_ZOHO_OAUTH_CLIENT_SECRET"
84205
84775
  };
84206
84776
  var CONNECTOR_ADAPTER_FACTORIES = [
84777
+ // Optional deployment override. The credential-only singleton remains the default.
84778
+ defineFactoryAdapter(createPhonyConnector, { baseUrl: "PH0NY_API_URL" }),
84207
84779
  defineFactoryAdapter(
84208
84780
  googleCalendar,
84209
84781
  googleOAuthEnvMap
@@ -84898,598 +85470,6 @@ function resolveConnectorAdapterFactoryOptions(definition, envSource) {
84898
85470
  return options;
84899
85471
  }
84900
85472
 
84901
- // src/connectors/adapters/phony.ts
84902
- var API15 = "https://api.ph0ny.com";
84903
- var E164 = /^\+[1-9]\d{7,14}$/;
84904
- var phonyConnector = {
84905
- manifest: {
84906
- kind: "phony",
84907
- displayName: "ph0ny",
84908
- description: "Place real outbound phone calls with a voice agent, then read back call status, transcript, and extracted fields. Outbound calls require recorded user consent and support a dry-run that validates the full configuration without dialing.",
84909
- auth: {
84910
- kind: "api-key",
84911
- hint: "Paste your ph0ny API key (plabs_\u2026). Create one in the developer portal via POST /v1/keys \u2014 it is shown once."
84912
- },
84913
- category: "comms",
84914
- // A call's status/transcript evolve while it is live, so a fetched row
84915
- // can be stale moments later — reads are point-in-time, not authoritative
84916
- // truth. start_outbound_call creates a fresh, uncontended call each time
84917
- // (cas='none', fire-and-forget external effect; no upstream CAS exists).
84918
- defaultConsistencyModel: "cache",
84919
- capabilities: [
84920
- {
84921
- name: "list_agents",
84922
- class: "read",
84923
- description: "List the voice agents on your ph0ny developer account (newest first).",
84924
- parameters: {
84925
- type: "object",
84926
- properties: {
84927
- limit: { type: "integer", minimum: 1, maximum: 100, default: 20 },
84928
- cursor: { type: "string", description: "Pagination cursor from a prior response." }
84929
- }
84930
- }
84931
- },
84932
- {
84933
- name: "get_call",
84934
- class: "read",
84935
- description: "Fetch a single outbound call by id, including status, transcript, and extracted fields.",
84936
- parameters: {
84937
- type: "object",
84938
- properties: {
84939
- id: { type: "string", description: "Outbound call id returned by start_outbound_call." }
84940
- },
84941
- required: ["id"]
84942
- }
84943
- },
84944
- {
84945
- name: "list_calls",
84946
- class: "read",
84947
- description: "List recent outbound calls for the account, optionally filtered by agent.",
84948
- parameters: {
84949
- type: "object",
84950
- properties: {
84951
- agentId: { type: "string", description: "Optional filter \u2014 only calls placed by this agent." },
84952
- limit: { type: "integer", minimum: 1, maximum: 50, default: 20 }
84953
- }
84954
- }
84955
- },
84956
- {
84957
- name: "start_outbound_call",
84958
- class: "mutation",
84959
- description: "Place an outbound phone call. Requires userConsentRecorded=true (the user must have explicitly authorized the call). Set dryRun=true to validate the full configuration without dialing.",
84960
- cas: "none",
84961
- externalEffect: true,
84962
- parameters: {
84963
- type: "object",
84964
- properties: {
84965
- agentId: { type: "string", description: "Agent that will place the call (agentKind must be personal_assistant)." },
84966
- toNumber: { type: "string", description: "E.164 destination, e.g. +14155551212." },
84967
- fromNumber: { type: "string", description: "E.164 caller number provisioned to your developer account." },
84968
- mission: {
84969
- type: "object",
84970
- description: "What the agent should accomplish on the call.",
84971
- properties: {
84972
- goal: { type: "string", description: "Plain-language objective (8\u20132000 chars)." },
84973
- successSchema: { type: "object", description: "Optional JSON schema describing fields to extract on success." },
84974
- ivrHints: {
84975
- type: "array",
84976
- items: { type: "string" },
84977
- description: "Optional hints for navigating phone-tree / IVR menus (\u22648 entries)."
84978
- },
84979
- maxTurns: { type: "integer", minimum: 1, maximum: 60 },
84980
- maxDurationMs: { type: "integer", minimum: 3e4, maximum: 12e5 }
84981
- },
84982
- required: ["goal"]
84983
- },
84984
- missionId: { type: "string", description: "Optional caller-supplied mission id." },
84985
- callerProfile: {
84986
- type: "object",
84987
- properties: {
84988
- userName: { type: "string" },
84989
- companyName: { type: "string" }
84990
- }
84991
- },
84992
- voiceCloneId: { type: "string", description: "Optional cloned-voice id to speak with." },
84993
- userConsentRecorded: {
84994
- type: "boolean",
84995
- description: "REQUIRED. Must be true \u2014 the user explicitly authorized this call. ph0ny rejects false."
84996
- },
84997
- dryRun: {
84998
- type: "boolean",
84999
- description: "When true, validate every gate and return a dryRunReport without placing the call."
85000
- }
85001
- },
85002
- required: ["agentId", "toNumber", "fromNumber", "mission", "userConsentRecorded"]
85003
- }
85004
- },
85005
- {
85006
- name: "create_agent",
85007
- class: "mutation",
85008
- description: "Create a voice agent on your ph0ny developer account. Only name is required; the rest configure the LLM, TTS, voice, knowledge base, and capture behavior.",
85009
- cas: "none",
85010
- externalEffect: true,
85011
- parameters: {
85012
- type: "object",
85013
- properties: {
85014
- id: { type: "string", description: "Optional caller-supplied agent id (1\u201360 chars)." },
85015
- name: { type: "string", description: "Display name (1\u2013100 chars)." },
85016
- description: { type: "string", description: "Optional description (\u22641000 chars)." },
85017
- systemPrompt: { type: "string", description: "Optional system prompt (\u226410000 chars)." },
85018
- firstMessage: { type: "string", description: "Optional opening line the agent speaks first (\u22641000 chars)." },
85019
- voiceId: { type: "string", description: "Optional TTS voice id." },
85020
- ttsProvider: { type: "string", description: 'TTS provider; "default" lets ph0ny choose.' },
85021
- ttsModel: { type: "string", description: "Optional TTS model id." },
85022
- sttProvider: { type: "string", description: "Optional speech-to-text provider." },
85023
- llmProvider: { type: "string", description: "LLM provider (e.g. openai, anthropic). Defaults to openai server-side." },
85024
- llmModel: { type: "string", description: "Optional LLM model id." },
85025
- libraryId: { type: "string", description: "Ingest-service library id (preferred over collectionId)." },
85026
- collectionId: { type: "string", description: "Deprecated \u2014 KB collection id to attach. Prefer libraryId." },
85027
- styleProfile: { type: "object", description: "Optional style profile object." },
85028
- promptByModel: { type: "object", description: "Optional per-model prompt overrides (modelId \u2192 prompt)." },
85029
- language: { type: "string", description: 'BCP-47 language code; defaults to "en".' },
85030
- temperature: { type: "number", minimum: 0, maximum: 2, description: "Sampling temperature (0\u20132); defaults to 0.7." },
85031
- maxTokens: { type: "integer", minimum: 1, maximum: 16384, description: "Optional max output tokens." },
85032
- contactCaptureEnabled: { type: "boolean", description: "Enable structured contact capture during calls." },
85033
- contactCaptureFields: {
85034
- type: "array",
85035
- items: { type: "string", enum: ["name", "email", "phone"] },
85036
- description: "Which contact fields to capture when capture is enabled."
85037
- },
85038
- metadata: { type: "object", description: "Arbitrary metadata object stored with the agent." }
85039
- },
85040
- required: ["name"]
85041
- }
85042
- },
85043
- {
85044
- name: "provision_agent",
85045
- class: "mutation",
85046
- description: "Create an agent and, in the same atomic call, optionally a fresh knowledge-base collection and seed it with initial content. Returns the agent plus the created collection and ingestion results.",
85047
- cas: "none",
85048
- externalEffect: true,
85049
- parameters: {
85050
- type: "object",
85051
- properties: {
85052
- id: { type: "string", description: "Optional caller-supplied agent id (1\u201360 chars)." },
85053
- name: { type: "string", description: "Display name (1\u2013100 chars)." },
85054
- description: { type: "string", description: "Optional description (\u22641000 chars)." },
85055
- systemPrompt: { type: "string", description: "Optional system prompt (\u226410000 chars)." },
85056
- firstMessage: { type: "string", description: "Optional opening line the agent speaks first (\u22641000 chars)." },
85057
- voiceId: { type: "string", description: "Optional TTS voice id." },
85058
- ttsProvider: { type: "string", description: 'TTS provider; "default" lets ph0ny choose.' },
85059
- ttsModel: { type: "string", description: "Optional TTS model id." },
85060
- sttProvider: { type: "string", description: "Optional speech-to-text provider." },
85061
- llmProvider: { type: "string", description: "LLM provider (e.g. openai, anthropic). Defaults to openai server-side." },
85062
- llmModel: { type: "string", description: "Optional LLM model id." },
85063
- libraryId: { type: "string", description: "Ingest-service library id (preferred over collectionId)." },
85064
- collectionId: { type: "string", description: "Existing KB collection id to attach instead of creating one." },
85065
- styleProfile: { type: "object", description: "Optional style profile object." },
85066
- promptByModel: { type: "object", description: "Optional per-model prompt overrides (modelId \u2192 prompt)." },
85067
- language: { type: "string", description: 'BCP-47 language code; defaults to "en".' },
85068
- temperature: { type: "number", minimum: 0, maximum: 2, description: "Sampling temperature (0\u20132); defaults to 0.7." },
85069
- maxTokens: { type: "integer", minimum: 1, maximum: 16384, description: "Optional max output tokens." },
85070
- contactCaptureEnabled: { type: "boolean", description: "Enable structured contact capture during calls." },
85071
- contactCaptureFields: {
85072
- type: "array",
85073
- items: { type: "string", enum: ["name", "email", "phone"] },
85074
- description: "Which contact fields to capture when capture is enabled."
85075
- },
85076
- metadata: { type: "object", description: "Arbitrary metadata object stored with the agent." },
85077
- collection: {
85078
- type: "object",
85079
- description: "When present, create a fresh KB collection and attach it to the agent.",
85080
- properties: {
85081
- name: { type: "string", description: "Collection name (1\u2013100 chars)." },
85082
- description: { type: "string", description: "Optional collection description (\u2264500 chars)." }
85083
- },
85084
- required: ["name"]
85085
- },
85086
- initialContent: {
85087
- type: "array",
85088
- maxItems: 10,
85089
- description: "Documents to ingest into the new/attached collection (\u226410).",
85090
- items: {
85091
- type: "object",
85092
- properties: {
85093
- content: { type: "string", description: "Raw text or transcript content." },
85094
- contentType: { type: "string", enum: ["text", "transcript"], description: 'Defaults to "text".' },
85095
- metadata: { type: "object", description: "Arbitrary metadata stored with the document." }
85096
- },
85097
- required: ["content"]
85098
- }
85099
- }
85100
- },
85101
- required: ["name"]
85102
- }
85103
- },
85104
- {
85105
- name: "kb_create_collection",
85106
- class: "mutation",
85107
- description: "Create a knowledge-base collection the agent can search at call time.",
85108
- cas: "none",
85109
- externalEffect: true,
85110
- parameters: {
85111
- type: "object",
85112
- properties: {
85113
- name: { type: "string", description: "Collection name (1\u2013100 chars)." },
85114
- description: { type: "string", description: "Optional description (\u2264500 chars)." },
85115
- metadata: { type: "object", description: "Arbitrary metadata object stored with the collection." }
85116
- },
85117
- required: ["name"]
85118
- }
85119
- },
85120
- {
85121
- name: "kb_ingest",
85122
- class: "mutation",
85123
- description: 'Chunk, embed, and store content into a KB collection. Provide content directly (\u22641MB), or a sourceUrl for audio transcription. contentType "url" is not yet implemented server-side (501).',
85124
- cas: "none",
85125
- externalEffect: true,
85126
- parameters: {
85127
- type: "object",
85128
- properties: {
85129
- collectionId: { type: "string", description: "Target collection id." },
85130
- content: { type: "string", description: "Raw text/transcript content (\u22641,000,000 chars)." },
85131
- sourceUrl: { type: "string", description: 'Public URL \u2014 required when contentType is "audio" (transcribed) or "url".' },
85132
- contentType: {
85133
- type: "string",
85134
- enum: ["text", "transcript", "audio", "url"],
85135
- description: 'Defaults to "text". "audio" transcribes sourceUrl; "url" returns 501.'
85136
- },
85137
- metadata: { type: "object", description: "Arbitrary metadata stored with the document." },
85138
- chunkSize: { type: "integer", minimum: 100, maximum: 4e3, description: "Chunk size in chars; defaults to 1000." },
85139
- chunkOverlap: { type: "integer", minimum: 0, maximum: 500, description: "Chunk overlap in chars; defaults to 200. Must be < chunkSize." }
85140
- },
85141
- required: ["collectionId"]
85142
- }
85143
- },
85144
- {
85145
- name: "kb_search",
85146
- class: "read",
85147
- description: "Hybrid vector + keyword search over a KB collection. Returns ranked results with scores.",
85148
- parameters: {
85149
- type: "object",
85150
- properties: {
85151
- collectionId: { type: "string", description: "Collection id to search." },
85152
- query: { type: "string", description: "Search query (1\u20132000 chars)." },
85153
- limit: { type: "integer", minimum: 1, maximum: 100, description: "Max results; defaults to 10." },
85154
- threshold: { type: "number", minimum: 0, maximum: 1, description: "Minimum similarity score; defaults to 0.7." },
85155
- includeMetadata: { type: "boolean", description: "Include per-result metadata; defaults to true." }
85156
- },
85157
- required: ["collectionId", "query"]
85158
- }
85159
- }
85160
- ]
85161
- },
85162
- async executeRead(inv) {
85163
- const token = bearerToken(inv.source.credentials);
85164
- if (inv.capabilityName === "list_agents") {
85165
- const { limit, cursor } = inv.args;
85166
- const params = new URLSearchParams();
85167
- params.set("limit", String(Math.min(Math.max(1, limit ?? 20), 100)));
85168
- if (cursor) params.set("cursor", cursor);
85169
- const json = await getJson(
85170
- inv,
85171
- token,
85172
- `${API15}/v1/agents?${params.toString()}`,
85173
- "list_agents"
85174
- );
85175
- return {
85176
- data: { agents: json.data ?? [], nextCursor: json.nextCursor ?? null, hasMore: json.hasMore ?? false },
85177
- fetchedAt: Date.now()
85178
- };
85179
- }
85180
- if (inv.capabilityName === "get_call") {
85181
- const { id: id7 } = inv.args;
85182
- const json = await getJson(
85183
- inv,
85184
- token,
85185
- `${API15}/v1/outbound/${encodeURIComponent(id7)}`,
85186
- "get_call"
85187
- );
85188
- return { data: { call: json.call ?? null }, fetchedAt: Date.now() };
85189
- }
85190
- if (inv.capabilityName === "list_calls") {
85191
- const { agentId, limit } = inv.args;
85192
- const params = new URLSearchParams();
85193
- params.set("limit", String(Math.min(Math.max(1, limit ?? 20), 50)));
85194
- if (agentId) params.set("agentId", agentId);
85195
- const json = await getJson(
85196
- inv,
85197
- token,
85198
- `${API15}/v1/outbound?${params.toString()}`,
85199
- "list_calls"
85200
- );
85201
- return { data: { calls: json.calls ?? [] }, fetchedAt: Date.now() };
85202
- }
85203
- if (inv.capabilityName === "kb_search") {
85204
- const { collectionId, query: query2, limit, threshold, includeMetadata } = inv.args;
85205
- const payload = { query: query2 };
85206
- if (limit !== void 0) payload.limit = limit;
85207
- if (threshold !== void 0) payload.threshold = threshold;
85208
- if (includeMetadata !== void 0) payload.includeMetadata = includeMetadata;
85209
- const res = await fetch(`${API15}/v1/collections/${encodeURIComponent(collectionId)}/search`, {
85210
- method: "POST",
85211
- headers: {
85212
- authorization: `Bearer ${token}`,
85213
- "content-type": "application/json"
85214
- },
85215
- body: JSON.stringify(payload),
85216
- signal: AbortSignal.timeout(15e3)
85217
- });
85218
- if (res.status === 401) throw new CredentialsExpired("ph0ny rejected credentials (401)", inv.source.id);
85219
- if (!res.ok) {
85220
- const text2 = await res.text().catch(() => "");
85221
- throw new Error(`phony kb_search ${res.status}: ${text2.slice(0, 200)}`);
85222
- }
85223
- const json = await res.json();
85224
- return {
85225
- data: {
85226
- results: json.results ?? [],
85227
- queryTokens: json.queryTokens ?? 0,
85228
- ...json.graphContext !== void 0 ? { graphContext: json.graphContext } : {}
85229
- },
85230
- fetchedAt: Date.now()
85231
- };
85232
- }
85233
- throw new Error(`phony: unknown read capability ${inv.capabilityName}`);
85234
- },
85235
- async executeMutation(inv) {
85236
- const token = bearerToken(inv.source.credentials);
85237
- if (inv.capabilityName === "start_outbound_call") {
85238
- const args = validateOutboundStartArgs(inv.args);
85239
- const payload = {
85240
- agentId: args.agentId,
85241
- toNumber: args.toNumber,
85242
- fromNumber: args.fromNumber,
85243
- mission: args.mission,
85244
- userConsentRecorded: args.userConsentRecorded
85245
- };
85246
- if (args.missionId !== void 0) payload.missionId = args.missionId;
85247
- if (args.callerProfile !== void 0) payload.callerProfile = args.callerProfile;
85248
- if (args.voiceCloneId !== void 0) payload.voiceCloneId = args.voiceCloneId;
85249
- if (args.dryRun !== void 0) payload.dryRun = args.dryRun;
85250
- const res = await fetch(`${API15}/v1/outbound/start`, {
85251
- method: "POST",
85252
- headers: {
85253
- authorization: `Bearer ${token}`,
85254
- "content-type": "application/json"
85255
- },
85256
- body: JSON.stringify(payload),
85257
- signal: AbortSignal.timeout(2e4)
85258
- });
85259
- if (res.status === 401) throw new CredentialsExpired("ph0ny rejected credentials (401)", inv.source.id);
85260
- if (!res.ok) {
85261
- const text2 = await res.text().catch(() => "");
85262
- throw new Error(`phony start_outbound_call ${res.status}: ${text2.slice(0, 200)}`);
85263
- }
85264
- const json = await res.json();
85265
- return {
85266
- status: "committed",
85267
- data: {
85268
- callId: json.callId,
85269
- callSid: json.callSid,
85270
- callStatus: json.status,
85271
- dryRun: json.dryRun ?? false,
85272
- ...json.dryRunReport !== void 0 ? { dryRunReport: json.dryRunReport } : {}
85273
- },
85274
- committedAt: Date.now(),
85275
- idempotentReplay: false
85276
- };
85277
- }
85278
- if (inv.capabilityName === "create_agent") {
85279
- const payload = pick3(inv.args, AGENT_FIELDS);
85280
- const json = await postJson(inv, token, `${API15}/v1/agents`, payload, "create_agent");
85281
- return {
85282
- status: "committed",
85283
- data: { agent: json },
85284
- committedAt: Date.now(),
85285
- idempotentReplay: false
85286
- };
85287
- }
85288
- if (inv.capabilityName === "provision_agent") {
85289
- const args = inv.args;
85290
- const payload = pick3(args, AGENT_FIELDS);
85291
- if (args.collection !== void 0) payload.collection = args.collection;
85292
- if (args.initialContent !== void 0) payload.initialContent = args.initialContent;
85293
- const json = await postJson(
85294
- inv,
85295
- token,
85296
- `${API15}/v1/agents/provision`,
85297
- payload,
85298
- "provision_agent"
85299
- );
85300
- return {
85301
- status: "committed",
85302
- data: {
85303
- agent: json.agent ?? null,
85304
- ...json.collection !== void 0 ? { collection: json.collection } : {},
85305
- ...json.ingested !== void 0 ? { ingested: json.ingested } : {}
85306
- },
85307
- committedAt: Date.now(),
85308
- idempotentReplay: false
85309
- };
85310
- }
85311
- if (inv.capabilityName === "kb_create_collection") {
85312
- const { name, description, metadata } = inv.args;
85313
- const payload = { name };
85314
- if (description !== void 0) payload.description = description;
85315
- if (metadata !== void 0) payload.metadata = metadata;
85316
- const json = await postJson(inv, token, `${API15}/v1/collections`, payload, "kb_create_collection");
85317
- return {
85318
- status: "committed",
85319
- data: { collection: json },
85320
- committedAt: Date.now(),
85321
- idempotentReplay: false
85322
- };
85323
- }
85324
- if (inv.capabilityName === "kb_ingest") {
85325
- const { collectionId, ...rest } = inv.args;
85326
- const payload = pick3(rest, INGEST_FIELDS);
85327
- const json = await postJson(
85328
- inv,
85329
- token,
85330
- `${API15}/v1/collections/${encodeURIComponent(collectionId)}/ingest`,
85331
- payload,
85332
- "kb_ingest"
85333
- );
85334
- return {
85335
- status: "committed",
85336
- data: {
85337
- documentId: json.documentId ?? null,
85338
- chunksCreated: json.chunksCreated ?? 0,
85339
- tokensUsed: json.tokensUsed ?? 0
85340
- },
85341
- committedAt: Date.now(),
85342
- idempotentReplay: false
85343
- };
85344
- }
85345
- throw new Error(`phony: unknown mutation capability ${inv.capabilityName}`);
85346
- },
85347
- async test(source) {
85348
- try {
85349
- const token = bearerToken(source.credentials);
85350
- const res = await fetch(`${API15}/v1/outbound?limit=1`, {
85351
- headers: { authorization: `Bearer ${token}` },
85352
- signal: AbortSignal.timeout(8e3)
85353
- });
85354
- if (res.status === 401) return { ok: false, reason: "ph0ny rejected credentials (401) \u2014 reconnect required" };
85355
- if (!res.ok) return { ok: false, reason: `ph0ny returned ${res.status}` };
85356
- return { ok: true };
85357
- } catch (err) {
85358
- return { ok: false, reason: err instanceof Error ? err.message : String(err) };
85359
- }
85360
- }
85361
- };
85362
- function bearerToken(creds) {
85363
- if (creds.kind !== "api-key" || typeof creds.apiKey !== "string" || creds.apiKey.length === 0) {
85364
- throw new Error("phony: expected api-key credentials");
85365
- }
85366
- return creds.apiKey;
85367
- }
85368
- function validateOutboundStartArgs(args) {
85369
- if (args.userConsentRecorded !== true) {
85370
- throw new Error(
85371
- "phony start_outbound_call requires userConsentRecorded=true after explicit user authorization; refusing before contacting ph0ny"
85372
- );
85373
- }
85374
- assertNonEmptyString(args.agentId, "agentId", 128);
85375
- assertE164(args.toNumber, "toNumber");
85376
- assertE164(args.fromNumber, "fromNumber");
85377
- if (!isRecord4(args.mission)) throw new Error("phony start_outbound_call requires mission");
85378
- const mission = args.mission;
85379
- assertNonEmptyString(mission.goal, "mission.goal", 2e3);
85380
- if (mission.goal.trim().length < 8) throw new Error("phony start_outbound_call mission.goal must be at least 8 characters");
85381
- if (mission.successSchema !== void 0 && !isRecord4(mission.successSchema)) {
85382
- throw new Error("phony start_outbound_call mission.successSchema must be an object when supplied");
85383
- }
85384
- if (mission.ivrHints !== void 0) {
85385
- if (!Array.isArray(mission.ivrHints) || mission.ivrHints.length > 8) {
85386
- throw new Error("phony start_outbound_call mission.ivrHints must be an array with at most 8 entries");
85387
- }
85388
- for (const hint of mission.ivrHints) assertNonEmptyString(hint, "mission.ivrHints[]", 200);
85389
- }
85390
- assertOptionalInteger(mission.maxTurns, "mission.maxTurns", 1, 60);
85391
- assertOptionalInteger(mission.maxDurationMs, "mission.maxDurationMs", 3e4, 20 * 60 * 1e3);
85392
- if (args.missionId !== void 0) assertNonEmptyString(args.missionId, "missionId", 128);
85393
- if (args.voiceCloneId !== void 0) assertNonEmptyString(args.voiceCloneId, "voiceCloneId", 128);
85394
- if (args.callerProfile !== void 0) {
85395
- if (!isRecord4(args.callerProfile)) throw new Error("phony start_outbound_call callerProfile must be an object");
85396
- if (args.callerProfile.userName !== void 0) assertNonEmptyString(args.callerProfile.userName, "callerProfile.userName", 120);
85397
- if (args.callerProfile.companyName !== void 0) {
85398
- assertNonEmptyString(args.callerProfile.companyName, "callerProfile.companyName", 120);
85399
- }
85400
- }
85401
- if (args.dryRun !== void 0 && typeof args.dryRun !== "boolean") {
85402
- throw new Error("phony start_outbound_call dryRun must be boolean when supplied");
85403
- }
85404
- return args;
85405
- }
85406
- function assertNonEmptyString(value, field, maxLength) {
85407
- if (typeof value !== "string" || value.trim().length === 0 || value.length > maxLength) {
85408
- throw new Error(`phony start_outbound_call ${field} must be a non-empty string <= ${maxLength} chars`);
85409
- }
85410
- }
85411
- function assertE164(value, field) {
85412
- if (typeof value !== "string" || !E164.test(value)) {
85413
- throw new Error(`phony start_outbound_call ${field} must be E.164 format, e.g. +14155550123`);
85414
- }
85415
- }
85416
- function assertOptionalInteger(value, field, min, max) {
85417
- if (value === void 0) return;
85418
- if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
85419
- throw new Error(`phony start_outbound_call ${field} must be an integer between ${min} and ${max}`);
85420
- }
85421
- }
85422
- function isRecord4(value) {
85423
- return typeof value === "object" && value !== null && !Array.isArray(value);
85424
- }
85425
- async function getJson(inv, token, url, label) {
85426
- const res = await fetch(url, {
85427
- headers: { authorization: `Bearer ${token}` },
85428
- signal: AbortSignal.timeout(1e4)
85429
- });
85430
- if (res.status === 401) throw new CredentialsExpired("ph0ny rejected credentials (401)", inv.source.id);
85431
- if (!res.ok) {
85432
- const text2 = await res.text().catch(() => "");
85433
- throw new Error(`phony ${label} ${res.status}: ${text2.slice(0, 200)}`);
85434
- }
85435
- return await res.json();
85436
- }
85437
- async function postJson(inv, token, url, payload, label) {
85438
- const res = await fetch(url, {
85439
- method: "POST",
85440
- headers: {
85441
- authorization: `Bearer ${token}`,
85442
- "content-type": "application/json"
85443
- },
85444
- body: JSON.stringify(payload),
85445
- signal: AbortSignal.timeout(2e4)
85446
- });
85447
- if (res.status === 401) throw new CredentialsExpired("ph0ny rejected credentials (401)", inv.source.id);
85448
- if (!res.ok) {
85449
- const text2 = await res.text().catch(() => "");
85450
- throw new Error(`phony ${label} ${res.status}: ${text2.slice(0, 200)}`);
85451
- }
85452
- return await res.json();
85453
- }
85454
- function pick3(args, fields) {
85455
- const out = {};
85456
- for (const key of fields) {
85457
- if (args[key] !== void 0) out[key] = args[key];
85458
- }
85459
- return out;
85460
- }
85461
- var AGENT_FIELDS = [
85462
- "id",
85463
- "name",
85464
- "description",
85465
- "systemPrompt",
85466
- "firstMessage",
85467
- "voiceId",
85468
- "ttsProvider",
85469
- "ttsModel",
85470
- "sttProvider",
85471
- "llmProvider",
85472
- "llmModel",
85473
- "libraryId",
85474
- "collectionId",
85475
- "styleProfile",
85476
- "promptByModel",
85477
- "language",
85478
- "temperature",
85479
- "maxTokens",
85480
- "contactCaptureEnabled",
85481
- "contactCaptureFields",
85482
- "metadata"
85483
- ];
85484
- var INGEST_FIELDS = [
85485
- "content",
85486
- "sourceUrl",
85487
- "contentType",
85488
- "metadata",
85489
- "chunkSize",
85490
- "chunkOverlap"
85491
- ];
85492
-
85493
85473
  // src/connectors/adapters/webhook.ts
85494
85474
  import { createHmac as createHmac6 } from "crypto";
85495
85475
  var webhookConnector = {
@@ -150505,6 +150485,8 @@ export {
150505
150485
  oauthClientCredentialRedactionValues,
150506
150486
  redactOAuthSensitiveText,
150507
150487
  _resetPendingFlowsForTests,
150488
+ createPhonyConnector,
150489
+ phonyConnector,
150508
150490
  declarativeRestConnector,
150509
150491
  oktaConnector,
150510
150492
  clientCredentialsRestConnector,
@@ -150831,7 +150813,6 @@ export {
150831
150813
  zerobounceConnector,
150832
150814
  CONNECTOR_ADAPTER_FACTORIES,
150833
150815
  resolveConnectorAdapterFactoryOptions,
150834
- phonyConnector,
150835
150816
  webhookConnector,
150836
150817
  stripeWebhookReceiverConnector,
150837
150818
  slackEventsConnector,
@@ -151168,4 +151149,4 @@ export {
151168
151149
  sendblueConnector,
151169
151150
  adapters_exports
151170
151151
  };
151171
- //# sourceMappingURL=chunk-ZD7FKHF5.js.map
151152
+ //# sourceMappingURL=chunk-RFJSN3YW.js.map