@fonoster/autopilot 0.30.0 → 0.31.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.
Files changed (53) hide show
  1. package/README.md +5 -5
  2. package/dist/knowledge/MeteredEmbeddings.d.ts +47 -0
  3. package/dist/knowledge/MeteredEmbeddings.js +58 -0
  4. package/dist/models/createLanguageModel.d.ts +2 -2
  5. package/dist/models/createLanguageModel.js +2 -2
  6. package/dist/models/evaluations/createEvalEffectiveConfig.d.ts +6 -6
  7. package/dist/models/evaluations/createEvalEffectiveConfig.js +7 -7
  8. package/dist/models/evaluations/createEvaluateIntelligence.d.ts +15 -4
  9. package/dist/models/evaluations/createEvaluateIntelligence.js +33 -12
  10. package/dist/models/evaluations/createTestTextSimilarity.d.ts +7 -1
  11. package/dist/models/evaluations/createTestTextSimilarity.js +19 -1
  12. package/dist/models/evaluations/evalTestCases.d.ts +2 -2
  13. package/dist/models/evaluations/evalTestCases.js +7 -7
  14. package/dist/models/evaluations/evaluateScenario.js +2 -2
  15. package/dist/models/evaluations/evaluateStep.d.ts +1 -1
  16. package/dist/models/evaluations/evaluateStep.js +2 -2
  17. package/dist/models/evaluations/extractAiResponse.d.ts +2 -2
  18. package/dist/models/evaluations/extractAiResponse.js +3 -3
  19. package/dist/models/evaluations/runEval.d.ts +2 -2
  20. package/dist/models/evaluations/runEval.js +7 -7
  21. package/dist/models/evaluations/session/EvalSession.d.ts +1 -1
  22. package/dist/models/evaluations/session/EvalSession.js +1 -1
  23. package/dist/models/evaluations/session/evaluateSessionScenario.d.ts +3 -3
  24. package/dist/models/evaluations/session/evaluateSessionScenario.js +4 -4
  25. package/dist/models/evaluations/session/runSessionEval.d.ts +7 -4
  26. package/dist/models/evaluations/session/runSessionEval.js +7 -7
  27. package/dist/models/evaluations/textSimilaryPrompt.d.ts +1 -1
  28. package/dist/models/evaluations/textSimilaryPrompt.js +1 -1
  29. package/dist/models/evaluations/types.d.ts +5 -5
  30. package/dist/models/types.d.ts +3 -3
  31. package/dist/session/Agent.d.ts +38 -3
  32. package/dist/session/Agent.js +90 -16
  33. package/dist/session/config.d.ts +2 -2
  34. package/dist/session/config.js +4 -4
  35. package/dist/session/createAutopilot.d.ts +12 -11
  36. package/dist/session/createAutopilot.js +48 -16
  37. package/dist/session/hosted.d.ts +23 -25
  38. package/dist/session/hosted.js +43 -48
  39. package/dist/session/index.d.ts +1 -1
  40. package/dist/session/index.js +1 -1
  41. package/dist/session/knowledge.d.ts +1 -1
  42. package/dist/session/llm.d.ts +28 -1
  43. package/dist/session/llm.js +50 -6
  44. package/dist/session/main.js +2 -1
  45. package/dist/session/meteredThink.d.ts +45 -0
  46. package/dist/session/meteredThink.js +125 -0
  47. package/dist/session/telemetry.d.ts +34 -0
  48. package/dist/session/telemetry.js +55 -0
  49. package/dist/session/tools.d.ts +1 -1
  50. package/dist/session/tools.js +1 -1
  51. package/package.json +9 -9
  52. package/dist/session/convertLegacyAssistant.d.ts +0 -77
  53. package/dist/session/convertLegacyAssistant.js +0 -118
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RegistryLlmResolver = exports.LlmResolutionError = exports.OPENAI_COMPATIBLE = exports.LayeredCredentials = exports.ConfigCredentials = void 0;
3
+ exports.RegistryLlmResolver = exports.LlmResolutionError = exports.OPENAI_COMPATIBLE = exports.LayeredCredentials = exports.KeyUnavailableError = exports.ConfigCredentials = void 0;
4
+ exports.resolveCredential = resolveCredential;
4
5
  const community_1 = require("@fonoster/community");
5
6
  const openai_1 = require("@fonoster/community/openai");
6
7
  Object.defineProperty(exports, "OPENAI_COMPATIBLE", { enumerable: true, get: function () { return openai_1.OPENAI_COMPATIBLE; } });
@@ -18,21 +19,58 @@ class ConfigCredentials {
18
19
  }
19
20
  }
20
21
  exports.ConfigCredentials = ConfigCredentials;
22
+ /**
23
+ * A workspace's own key could not be read (its store failed). It never falls back to the platform
24
+ * key (remove-platform-key-fallback): the session ends with reason key_unavailable.
25
+ */
26
+ class KeyUnavailableError extends Error {
27
+ constructor(ref, cause) {
28
+ super(`the workspace's key ${ref} could not be read: ${cause?.message ?? cause}`);
29
+ this.ref = ref;
30
+ this.reason = "key_unavailable";
31
+ }
32
+ }
33
+ exports.KeyUnavailableError = KeyUnavailableError;
21
34
  /** Workspace Secrets (BYOK) in front of platform credentials. */
22
35
  class LayeredCredentials {
23
36
  constructor(sources) {
24
37
  this.sources = sources;
25
38
  }
26
39
  async get(ref, scope) {
40
+ return (await this.resolve(ref, scope))?.key;
41
+ }
42
+ /**
43
+ * The first key found and whose it is (add-usage-metering): the operator's config holds the
44
+ * platform keys; every other source holds the workspace's own.
45
+ */
46
+ async resolve(ref, scope) {
27
47
  for (const s of this.sources) {
28
- const v = await s.get(ref, scope);
29
- if (v)
30
- return v;
48
+ let key;
49
+ try {
50
+ key = await s.get(ref, scope);
51
+ }
52
+ catch (e) {
53
+ if (s instanceof ConfigCredentials)
54
+ throw e;
55
+ throw new KeyUnavailableError(ref, e);
56
+ }
57
+ if (key)
58
+ return {
59
+ key,
60
+ source: s instanceof ConfigCredentials ? "platform" : "workspace"
61
+ };
31
62
  }
32
63
  return undefined;
33
64
  }
34
65
  }
35
66
  exports.LayeredCredentials = LayeredCredentials;
67
+ /** A credential and its source; a source that cannot tell is the platform's. */
68
+ async function resolveCredential(credentials, ref, scope) {
69
+ if (credentials instanceof LayeredCredentials)
70
+ return credentials.resolve(ref, scope);
71
+ const key = await credentials.get(ref, scope);
72
+ return key ? { key, source: "platform" } : undefined;
73
+ }
36
74
  class LlmResolutionError extends Error {
37
75
  }
38
76
  exports.LlmResolutionError = LlmResolutionError;
@@ -59,14 +97,20 @@ class RegistryLlmResolver {
59
97
  throw new LlmResolutionError(`adapter ${provider.adapter} has no language model implementation in Autopilot yet`);
60
98
  const baseUrl = this.options.endpoints?.[provider.id] ??
61
99
  this.options.endpoints?.[provider.adapter] ??
100
+ provider.endpoints?.llm ??
62
101
  llm.defaultEndpoint;
63
102
  if (!baseUrl)
64
103
  throw new LlmResolutionError(`no endpoint for provider ${provider.id} (adapter ${provider.adapter})`);
65
- const apiKey = await this.options.credentials.get(provider.credentialRef, scope);
66
- if (!apiKey && !(0, community_1.adapterFor)(provider.adapter)?.keyless)
104
+ // A provider without a credentialRef is keyless (local models, the fakes).
105
+ const credential = provider.credentialRef
106
+ ? await resolveCredential(this.options.credentials, provider.credentialRef, scope)
107
+ : undefined;
108
+ const apiKey = credential?.key;
109
+ if (provider.credentialRef && !apiKey)
67
110
  throw new LlmResolutionError(`no credential ${provider.credentialRef} for provider ${provider.id}`);
68
111
  return {
69
112
  id,
113
+ keySource: credential?.source ?? "platform",
70
114
  think: llm.create({ baseUrl, apiKey }, id.slice(providerId.length + 1), settings),
71
115
  ...(resolution.event
72
116
  ? {
@@ -24,6 +24,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
24
24
  const common_1 = require("@fonoster/common");
25
25
  const logger_1 = require("@fonoster/logger");
26
26
  const hosted_1 = require("./hosted");
27
+ const telemetry_1 = require("./telemetry");
27
28
  const logger = (0, logger_1.getLogger)({ service: "autopilot", filePath: __filename });
28
29
  (0, common_1.logUnhandledRejections)(logger);
29
30
  // --config, AUTOPILOT_SESSION_CONFIG, else /config/autopilot.yaml (or an older location).
@@ -37,7 +38,7 @@ if (!file) {
37
38
  }
38
39
  const config = (0, hosted_1.loadHostedConfig)(file);
39
40
  // Off unless an OTLP endpoint is configured (telemetry.otlpEndpoint or OTEL_EXPORTER_OTLP_*).
40
- (0, common_1.initTelemetry)("fonoster-autopilot", undefined, (0, common_1.telemetryEnv)(config.telemetry));
41
+ (0, telemetry_1.startTelemetry)(config.telemetry);
41
42
  (0, hosted_1.startHostedAutopilot)(config).catch((e) => {
42
43
  logger.error(`hosted autopilot failed to start: ${e.message}`);
43
44
  process.exit(1);
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
3
+ * http://github.com/fonoster/fonoster
4
+ *
5
+ * This file is part of Fonoster
6
+ *
7
+ * Licensed under the MIT License (the "License");
8
+ * you may not use this file except in compliance with
9
+ * the License. You may obtain a copy of the License at
10
+ *
11
+ * https://opensource.org/licenses/MIT
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+ import { Usage } from "@fonoster/common";
20
+ import { Think, TokenUsage } from "@fonoster/community";
21
+ /** Where a model's usage goes: Autopilot sends it to the call (usage.report). */
22
+ export type UsageSink = {
23
+ usage(lines: Usage.UsageLine[]): void;
24
+ /** A completed request whose vendor reported no usage (add-usage-metering). */
25
+ missing(catalogId: string): void;
26
+ };
27
+ export type MeteredModel = {
28
+ /** The catalog id (`<provider>/<model>`). */
29
+ catalogId: string;
30
+ keySource: Usage.KeySource;
31
+ };
32
+ /** How long an interrupted reply is read in the background for its usage report (design D6). */
33
+ export declare const DRAIN_DEADLINE_MS = 30000;
34
+ export declare function usageLines(model: MeteredModel, usage: TokenUsage): Usage.UsageLine[];
35
+ /**
36
+ * A Think that meters its model (add-usage-metering D6). Chunks pass through; the vendor's usage
37
+ * chunk is reported, never forwarded. When the caller stops reading (barge-in, a newer turn), the
38
+ * stream ends for the caller at once but keeps being read in the background, so the vendor's usage
39
+ * for the whole request still arrives; after the deadline, or when `closed` aborts (the session
40
+ * ended), the request is reported as missing usage. A request the vendor failed reports nothing.
41
+ */
42
+ export declare function meteredThink(inner: Think, model: MeteredModel, sink: UsageSink, options?: {
43
+ drainMs?: number;
44
+ closed?: AbortSignal;
45
+ }): Think;
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DRAIN_DEADLINE_MS = void 0;
4
+ exports.usageLines = usageLines;
5
+ exports.meteredThink = meteredThink;
6
+ /** How long an interrupted reply is read in the background for its usage report (design D6). */
7
+ exports.DRAIN_DEADLINE_MS = 30000;
8
+ const abortError = () => {
9
+ const e = new Error("aborted");
10
+ e.name = "AbortError";
11
+ return e;
12
+ };
13
+ /** Resolves like `p`, or rejects with an AbortError as soon as `signal` aborts. */
14
+ function raceAbort(p, signal) {
15
+ return new Promise((resolve, reject) => {
16
+ const onAbort = () => reject(abortError());
17
+ if (signal.aborted)
18
+ return onAbort();
19
+ signal.addEventListener("abort", onAbort, { once: true });
20
+ p.then((v) => {
21
+ signal.removeEventListener("abort", onAbort);
22
+ resolve(v);
23
+ }, (e) => {
24
+ signal.removeEventListener("abort", onAbort);
25
+ reject(e);
26
+ });
27
+ });
28
+ }
29
+ function usageLines(model, usage) {
30
+ const key = {
31
+ provider: model.catalogId.split("/")[0],
32
+ model: model.catalogId,
33
+ keySource: model.keySource
34
+ };
35
+ const lines = [
36
+ { meter: "llm.tokens.in", quantity: usage.inputTokens, ...key },
37
+ { meter: "llm.tokens.out", quantity: usage.outputTokens, ...key }
38
+ ];
39
+ if (usage.cachedTokens > 0)
40
+ lines.push({
41
+ meter: "llm.tokens.cached",
42
+ quantity: usage.cachedTokens,
43
+ ...key
44
+ });
45
+ return lines;
46
+ }
47
+ /**
48
+ * A Think that meters its model (add-usage-metering D6). Chunks pass through; the vendor's usage
49
+ * chunk is reported, never forwarded. When the caller stops reading (barge-in, a newer turn), the
50
+ * stream ends for the caller at once but keeps being read in the background, so the vendor's usage
51
+ * for the whole request still arrives; after the deadline, or when `closed` aborts (the session
52
+ * ended), the request is reported as missing usage. A request the vendor failed reports nothing.
53
+ */
54
+ function meteredThink(inner, model, sink, options = {}) {
55
+ const settle = (usage) => usage
56
+ ? sink.usage(usageLines(model, usage))
57
+ : sink.missing(model.catalogId);
58
+ return {
59
+ async *stream(messages, tools, signal) {
60
+ const request = new AbortController();
61
+ const stopRequest = () => request.abort();
62
+ options.closed?.addEventListener("abort", stopRequest, { once: true });
63
+ const it = inner
64
+ .stream(messages, tools, request.signal)[Symbol.asyncIterator]();
65
+ let usage;
66
+ let pending;
67
+ let state = "reading";
68
+ const done = () => options.closed?.removeEventListener("abort", stopRequest);
69
+ const drain = async () => {
70
+ const deadline = setTimeout(stopRequest, options.drainMs ?? exports.DRAIN_DEADLINE_MS);
71
+ try {
72
+ for (;;) {
73
+ const r = await (pending ?? it.next());
74
+ pending = undefined;
75
+ if (r.done)
76
+ break;
77
+ if (r.value.type === "usage")
78
+ usage = r.value.usage;
79
+ }
80
+ }
81
+ catch {
82
+ // Cut by the deadline or the session's end: whatever arrived is all there is.
83
+ }
84
+ finally {
85
+ clearTimeout(deadline);
86
+ done();
87
+ }
88
+ settle(usage);
89
+ };
90
+ try {
91
+ for (;;) {
92
+ pending = it.next();
93
+ pending.catch(() => undefined);
94
+ const r = await raceAbort(pending, signal);
95
+ pending = undefined;
96
+ if (r.done) {
97
+ state = "done";
98
+ break;
99
+ }
100
+ if (r.value.type === "usage")
101
+ usage = r.value.usage;
102
+ else
103
+ yield r.value;
104
+ }
105
+ }
106
+ catch (e) {
107
+ if (!signal.aborted)
108
+ state = "failed";
109
+ throw e;
110
+ }
111
+ finally {
112
+ if (state === "done") {
113
+ done();
114
+ settle(usage);
115
+ }
116
+ else if (state === "failed") {
117
+ done();
118
+ }
119
+ else {
120
+ void drain();
121
+ }
122
+ }
123
+ }
124
+ };
125
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
3
+ * http://github.com/fonoster/fonoster
4
+ *
5
+ * This file is part of Fonoster
6
+ *
7
+ * Licensed under the MIT License (the "License");
8
+ * you may not use this file except in compliance with
9
+ * the License. You may obtain a copy of the License at
10
+ *
11
+ * https://opensource.org/licenses/MIT
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+ import { Telemetry, TelemetryConfig } from "@fonoster/common";
20
+ /**
21
+ * Autopilot's telemetry: a stable handle (like AppBridge's) that delegates to what
22
+ * `startTelemetry`/`setTelemetry` installed. OTLP when an endpoint is configured
23
+ * (telemetry.otlpEndpoint or OTEL_EXPORTER_OTLP_*), a no-op otherwise.
24
+ *
25
+ * Spans: autopilot.session (child of AppBridge's call span, via session.start `traceparent`),
26
+ * autopilot.turn (STT final to the end of the reply) and autopilot.llm.request. Metrics:
27
+ * fonoster.autopilot.turns, fonoster.autopilot.llm.requests and fonoster.autopilot.llm.latency_ms
28
+ * (the sum of the time to the first token; divide by requests for the mean).
29
+ */
30
+ export declare const telemetry: Telemetry;
31
+ /** Installs a telemetry implementation (tests). */
32
+ export declare function setTelemetry(t: Telemetry): void;
33
+ /** Starts telemetry with the config's endpoint, once the config is loaded. */
34
+ export declare function startTelemetry(config: TelemetryConfig): void;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.telemetry = void 0;
4
+ exports.setTelemetry = setTelemetry;
5
+ exports.startTelemetry = startTelemetry;
6
+ /**
7
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
8
+ * http://github.com/fonoster/fonoster
9
+ *
10
+ * This file is part of Fonoster
11
+ *
12
+ * Licensed under the MIT License (the "License");
13
+ * you may not use this file except in compliance with
14
+ * the License. You may obtain a copy of the License at
15
+ *
16
+ * https://opensource.org/licenses/MIT
17
+ *
18
+ * Unless required by applicable law or agreed to in writing, software
19
+ * distributed under the License is distributed on an "AS IS" BASIS,
20
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21
+ * See the License for the specific language governing permissions and
22
+ * limitations under the License.
23
+ */
24
+ const common_1 = require("@fonoster/common");
25
+ let impl = (0, common_1.initTelemetry)("fonoster-autopilot");
26
+ /**
27
+ * Autopilot's telemetry: a stable handle (like AppBridge's) that delegates to what
28
+ * `startTelemetry`/`setTelemetry` installed. OTLP when an endpoint is configured
29
+ * (telemetry.otlpEndpoint or OTEL_EXPORTER_OTLP_*), a no-op otherwise.
30
+ *
31
+ * Spans: autopilot.session (child of AppBridge's call span, via session.start `traceparent`),
32
+ * autopilot.turn (STT final to the end of the reply) and autopilot.llm.request. Metrics:
33
+ * fonoster.autopilot.turns, fonoster.autopilot.llm.requests and fonoster.autopilot.llm.latency_ms
34
+ * (the sum of the time to the first token; divide by requests for the mean).
35
+ */
36
+ exports.telemetry = {
37
+ get enabled() {
38
+ return impl.enabled;
39
+ },
40
+ startCall: (...a) => impl.startCall(...a),
41
+ startSpan: (...a) => impl.startSpan(...a),
42
+ traceparent: (...a) => impl.traceparent(...a),
43
+ log: (...a) => impl.log(...a),
44
+ count: (...a) => impl.count(...a),
45
+ gauge: (...a) => impl.gauge(...a),
46
+ shutdown: () => impl.shutdown()
47
+ };
48
+ /** Installs a telemetry implementation (tests). */
49
+ function setTelemetry(t) {
50
+ impl = t;
51
+ }
52
+ /** Starts telemetry with the config's endpoint, once the config is loaded. */
53
+ function startTelemetry(config) {
54
+ impl = (0, common_1.initTelemetry)("fonoster-autopilot", undefined, (0, common_1.telemetryEnv)(config));
55
+ }
@@ -10,7 +10,7 @@ export interface ToolPort {
10
10
  run(name: string, args: Record<string, unknown>, signal: AbortSignal): Promise<ToolOutcome>;
11
11
  }
12
12
  /**
13
- * HTTP tools from the assistant config (`operation.method`, `url`, `headers`). Tool URLs are
13
+ * HTTP tools from the agent config (`operation.method`, `url`, `headers`). Tool URLs are
14
14
  * tenant input: they go through safeFetch (public destinations only, redirects re-checked,
15
15
  * 10 s timeout; RV-B2).
16
16
  */
@@ -22,7 +22,7 @@ exports.HttpToolPort = void 0;
22
22
  const url_1 = require("url");
23
23
  const common_1 = require("@fonoster/common");
24
24
  /**
25
- * HTTP tools from the assistant config (`operation.method`, `url`, `headers`). Tool URLs are
25
+ * HTTP tools from the agent config (`operation.method`, `url`, `headers`). Tool URLs are
26
26
  * tenant input: they go through safeFetch (public destinations only, redirects re-checked,
27
27
  * 10 s timeout; RV-B2).
28
28
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonoster/autopilot",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "Voice AI for the Fonoster platform",
5
5
  "author": "Pedro Sanders <psanders@fonoster.com>",
6
6
  "homepage": "https://github.com/fonoster/fonoster#readme",
@@ -14,8 +14,7 @@
14
14
  "scripts": {
15
15
  "prebuild": "rimraf ./dist tsconfig.tsbuildinfo",
16
16
  "build": "tsc -b tsconfig.json",
17
- "clean": "rimraf ./dist node_modules tsconfig.tsbuildinfo",
18
- "generate:readme": "node ../../.scripts/gen-readme.cjs",
17
+ "generate:readme": "fonoster-readme",
19
18
  "start:hosted": "node dist/session/main.js"
20
19
  },
21
20
  "files": [
@@ -34,11 +33,11 @@
34
33
  "dependencies": {
35
34
  "@aws-sdk/client-s3": "^3.958.0",
36
35
  "@dmitryrechkin/json-schema-to-zod": "^1.0.1",
37
- "@fonoster/common": "^0.30.0",
38
- "@fonoster/community": "^0.30.0",
39
- "@fonoster/logger": "^0.30.0",
40
- "@fonoster/types": "^0.30.0",
41
- "@fonoster/voice": "^0.30.0",
36
+ "@fonoster/common": "^0.31.0",
37
+ "@fonoster/community": "^0.31.0",
38
+ "@fonoster/logger": "^0.31.0",
39
+ "@fonoster/types": "^0.31.0",
40
+ "@fonoster/voice": "^0.31.0",
42
41
  "@grpc/grpc-js": "~1.14.6",
43
42
  "@langchain/anthropic": "^1.3.3",
44
43
  "@langchain/community": "^1.1.1",
@@ -52,10 +51,11 @@
52
51
  "js-yaml": "^4.1.1",
53
52
  "langchain": "^1.2.3",
54
53
  "pdf-parse": "^1.1.1",
54
+ "undici": "^7.30.1",
55
55
  "xstate": "^5.17.3",
56
56
  "zod": "^3.25.76"
57
57
  },
58
- "gitHead": "b59b06a3ef325abe3f188c0fb87cc22d50ed5471",
58
+ "gitHead": "f0a5102dddeea0f70f659234108805bc10690f2e",
59
59
  "devDependencies": {
60
60
  "yaml": "^2.9.0"
61
61
  }
@@ -1,77 +0,0 @@
1
- /**
2
- * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
3
- * http://github.com/fonoster/fonoster
4
- *
5
- * This file is part of Fonoster
6
- *
7
- * Licensed under the MIT License (the "License");
8
- * you may not use this file except in compliance with
9
- * the License. You may obtain a copy of the License at
10
- *
11
- * https://opensource.org/licenses/MIT
12
- *
13
- * Unless required by applicable law or agreed to in writing, software
14
- * distributed under the License is distributed on an "AS IS" BASIS,
15
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- * See the License for the specific language governing permissions and
17
- * limitations under the License.
18
- */
19
- import { SessionAssistant } from "./config";
20
- type Legacy = {
21
- conversationSettings?: Record<string, unknown> & {
22
- maxSpeechWaitTimeout?: number;
23
- vad?: unknown;
24
- transferOptions?: {
25
- phoneNumber: string;
26
- message: string;
27
- timeout?: number;
28
- };
29
- };
30
- languageModel?: Record<string, unknown> & {
31
- provider?: string;
32
- model?: string;
33
- apiKey?: string;
34
- baseUrl?: string;
35
- };
36
- eventsHook?: {
37
- url: string;
38
- events?: string[];
39
- headers?: Record<string, string>;
40
- };
41
- audioFilters?: unknown;
42
- testCases?: unknown;
43
- };
44
- /**
45
- * VAD-era settings → the patience knob (add-turn-taking 3.2, AP-B3). maxSpeechWaitTimeout was
46
- * extra waiting after the caller's speech before answering; 0 (the old default) is today's
47
- * behaviour, normal. Any extra wait becomes high (about 1.6 × the gateway's silence timeout, and
48
- * more as the gateway adapts to a caller who pauses). Nothing maps to low: the old settings had
49
- * no way to answer faster than the default. allowUserBargeIn keeps its meaning: it sets
50
- * `interruptible` on every speak and whether Autopilot sends clear on turn.user_started.
51
- */
52
- export declare function patienceFor(maxSpeechWaitTimeout?: number): "low" | "normal" | "high";
53
- export type AssistantConversion = {
54
- assistant: SessionAssistant;
55
- /**
56
- * An inline key to store as the workspace's key for `providerId` (the config no longer holds
57
- * it). The migration job binds it like the dashboard's Vendor keys page does (a Secret named
58
- * `provider-key/<provider>` in workspace_provider_keys), which is where the registry's
59
- * credentialRef lookup finds workspace keys (AP-B4, LG-B1).
60
- */
61
- secret?: {
62
- providerId: string;
63
- name: string;
64
- value: string;
65
- };
66
- /** The retired eventsHook URL, for creating a workspace webhook endpoint. */
67
- webhook?: {
68
- url: string;
69
- };
70
- /** Audio filters that now belong in the app's configuration. */
71
- appFilters?: unknown;
72
- notices: string[];
73
- };
74
- export declare function convertLegacyAssistant(old: Legacy, options: {
75
- assistantRef: string;
76
- }): AssistantConversion;
77
- export {};
@@ -1,118 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.patienceFor = patienceFor;
4
- exports.convertLegacyAssistant = convertLegacyAssistant;
5
- /**
6
- * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
7
- * http://github.com/fonoster/fonoster
8
- *
9
- * This file is part of Fonoster
10
- *
11
- * Licensed under the MIT License (the "License");
12
- * you may not use this file except in compliance with
13
- * the License. You may obtain a copy of the License at
14
- *
15
- * https://opensource.org/licenses/MIT
16
- *
17
- * Unless required by applicable law or agreed to in writing, software
18
- * distributed under the License is distributed on an "AS IS" BASIS,
19
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20
- * See the License for the specific language governing permissions and
21
- * limitations under the License.
22
- */
23
- const config_1 = require("./config");
24
- // The one-time conversion of stored assistants at cutover (owner decision, rework-autopilot 5.3).
25
- // It never writes anything itself: it returns the new config plus what the migration job must do
26
- // around it (create a workspace Secret, a webhook endpoint, app audio filters) and notices for the
27
- // workspace.
28
- const PROVIDERS = ["openai", "groq", "ollama", "google", "anthropic"];
29
- const PLACEHOLDER_KEYS = new Set(["", "your-api-key", "changeme", "xxx"]);
30
- /**
31
- * VAD-era settings → the patience knob (add-turn-taking 3.2, AP-B3). maxSpeechWaitTimeout was
32
- * extra waiting after the caller's speech before answering; 0 (the old default) is today's
33
- * behaviour, normal. Any extra wait becomes high (about 1.6 × the gateway's silence timeout, and
34
- * more as the gateway adapts to a caller who pauses). Nothing maps to low: the old settings had
35
- * no way to answer faster than the default. allowUserBargeIn keeps its meaning: it sets
36
- * `interruptible` on every speak and whether Autopilot sends clear on turn.user_started.
37
- */
38
- function patienceFor(maxSpeechWaitTimeout) {
39
- return (maxSpeechWaitTimeout ?? 0) > 0 ? "high" : "normal";
40
- }
41
- const DEFAULT_VAD = {
42
- activationThreshold: 0.4,
43
- deactivationThreshold: 0.25,
44
- debounceFrames: 4
45
- };
46
- function isDefaultVad(vad) {
47
- if (!vad || typeof vad !== "object")
48
- return true;
49
- const v = vad;
50
- return Object.keys(DEFAULT_VAD).every((k) => v[k] === undefined || v[k] === DEFAULT_VAD[k]);
51
- }
52
- function convertLegacyAssistant(old, options) {
53
- const notices = [];
54
- const lm = { ...(old.languageModel ?? {}) };
55
- const cs = { ...(old.conversationSettings ?? {}) };
56
- const result = { notices };
57
- const provider = String(lm.provider ?? "");
58
- if (!PROVIDERS.includes(provider))
59
- throw new Error(`unknown language model provider "${provider}"`);
60
- const model = `${provider}/${lm.model}`;
61
- if (typeof lm.apiKey === "string" &&
62
- !PLACEHOLDER_KEYS.has(lm.apiKey.trim())) {
63
- void options.assistantRef;
64
- result.secret = {
65
- providerId: provider,
66
- name: `provider-key/${provider}`,
67
- value: lm.apiKey
68
- };
69
- notices.push(`The ${provider} key moved out of the assistant into the workspace's ${provider} vendor key (Secret "${result.secret.name}").`);
70
- }
71
- if (lm.baseUrl) {
72
- notices.push(`The custom model endpoint ${lm.baseUrl} was removed; endpoints now come from the provider registry.`);
73
- }
74
- const patience = patienceFor(cs.maxSpeechWaitTimeout);
75
- if (patience !== "normal") {
76
- notices.push(`maxSpeechWaitTimeout (${cs.maxSpeechWaitTimeout} ms) became patience "${patience}"; turn timing now comes from the gateway.`);
77
- }
78
- if (cs.vad !== undefined && !isDefaultVad(cs.vad))
79
- notices.push("Custom VAD settings were removed; turn detection now runs in the gateway. For noisy lines use the app's audio filters or a more patient setting.");
80
- if (cs.transferOptions?.timeout !== undefined &&
81
- cs.transferOptions.timeout !== 30000) {
82
- notices.push(`The transfer timeout (${cs.transferOptions.timeout} ms) is no longer configurable per assistant.`);
83
- }
84
- if (old.eventsHook?.url) {
85
- result.webhook = { url: old.eventsHook.url };
86
- notices.push(`Assistant webhooks are retired; create a workspace webhook for ${old.eventsHook.url} (call.started, call.ended, call_record.sealed).`);
87
- }
88
- if (old.audioFilters !== undefined) {
89
- result.appFilters = old.audioFilters;
90
- notices.push("Audio filters moved to the app's configuration.");
91
- }
92
- const { maxSpeechWaitTimeout, vad, transferOptions, ...settings } = cs;
93
- void vad;
94
- void maxSpeechWaitTimeout;
95
- const assistant = config_1.sessionAssistantSchema.parse({
96
- conversationSettings: {
97
- ...settings,
98
- ...(transferOptions
99
- ? {
100
- transferOptions: {
101
- phoneNumber: transferOptions.phoneNumber,
102
- message: transferOptions.message
103
- }
104
- }
105
- : {}),
106
- ...(patience !== "normal" ? { patience } : {})
107
- },
108
- languageModel: {
109
- model,
110
- temperature: lm.temperature,
111
- maxTokens: lm.maxTokens,
112
- tools: lm.tools,
113
- knowledgeBase: lm.knowledgeBase
114
- },
115
- ...(old.testCases !== undefined ? { testCases: old.testCases } : {})
116
- });
117
- return { ...result, assistant };
118
- }