@byte-42/fantastico-sdk 0.0.0-stage → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +484 -3
- package/dist/activity.d.ts +72 -0
- package/dist/activity.js +22 -0
- package/dist/auth.d.ts +216 -0
- package/dist/auth.js +224 -0
- package/dist/backoff.d.ts +48 -0
- package/dist/backoff.js +31 -0
- package/dist/browser-actions.d.ts +28 -0
- package/dist/browser-actions.js +25 -0
- package/dist/client.d.ts +410 -0
- package/dist/client.js +410 -0
- package/dist/decisions.d.ts +149 -0
- package/dist/decisions.js +71 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +7 -0
- package/dist/llm.d.ts +40 -0
- package/dist/llm.js +74 -0
- package/dist/local-tools.d.ts +72 -0
- package/dist/local-tools.js +55 -0
- package/dist/max-tokens.d.ts +27 -0
- package/dist/max-tokens.js +16 -0
- package/dist/mcp.d.ts +14 -0
- package/dist/mcp.js +55 -0
- package/dist/orchestrator.d.ts +105 -0
- package/dist/orchestrator.js +296 -0
- package/dist/provider-error.d.ts +165 -0
- package/dist/provider-error.js +156 -0
- package/dist/raw.d.ts +48 -0
- package/dist/raw.js +72 -0
- package/dist/schema-digest.d.ts +25 -0
- package/dist/schema-digest.js +20 -0
- package/dist/speech.d.ts +107 -0
- package/dist/speech.js +48 -0
- package/dist/surface.d.ts +35 -0
- package/dist/surface.js +13 -0
- package/dist/tool-definition.d.ts +22 -0
- package/dist/tool-definition.js +10 -0
- package/dist/tool-origin.d.ts +32 -0
- package/dist/tool-origin.js +65 -0
- package/dist/types.d.ts +742 -0
- package/dist/types.js +27 -0
- package/package.json +48 -6
package/dist/llm.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ToolDefinition, ReasoningConfig } from "./types.js";
|
|
2
|
+
import type { LlmClient, LlmResponse, InternalMessage } from "./orchestrator.js";
|
|
3
|
+
import { type RetryConfig } from "./backoff.js";
|
|
4
|
+
export interface OpenRouterClientOptions {
|
|
5
|
+
apiKey: string;
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
referer?: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
fetchImpl?: typeof fetch;
|
|
10
|
+
/** Ritentativi sugli errori transitori. `false` li spegne. Vedi `backoff.ts`. */
|
|
11
|
+
retry?: RetryConfig | false;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Client del provider LLM — l'unico percorso di inferenza, per ogni modello. Chiede `usage: {include: true}` così la risposta
|
|
15
|
+
* porta il **costo reale in USD**, che alimenta la regolazione del budget senza stimarlo.
|
|
16
|
+
*/
|
|
17
|
+
export declare class OpenRouterLlmClient implements LlmClient {
|
|
18
|
+
private readonly baseUrl;
|
|
19
|
+
private readonly apiKey;
|
|
20
|
+
private readonly referer?;
|
|
21
|
+
private readonly title?;
|
|
22
|
+
private readonly fetchImpl;
|
|
23
|
+
private readonly retry;
|
|
24
|
+
constructor(opts: OpenRouterClientOptions);
|
|
25
|
+
chatCompletion(params: {
|
|
26
|
+
model: string;
|
|
27
|
+
messages: InternalMessage[];
|
|
28
|
+
tools?: ToolDefinition[];
|
|
29
|
+
temperature?: number;
|
|
30
|
+
max_tokens?: number;
|
|
31
|
+
reasoning?: ReasoningConfig;
|
|
32
|
+
}): Promise<LlmResponse>;
|
|
33
|
+
}
|
|
34
|
+
/** Crea il client di inferenza. Un solo percorso: il provider LLM con la runtime key del progetto. */
|
|
35
|
+
export declare function createLlmClient(opts: {
|
|
36
|
+
providerApiKey: string;
|
|
37
|
+
baseUrl?: string;
|
|
38
|
+
fetchImpl?: typeof fetch;
|
|
39
|
+
retry?: RetryConfig | false;
|
|
40
|
+
}): LlmClient;
|
package/dist/llm.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { providerErrorFromResponse, errorInBody, emptyCompletionError } from "./provider-error.js";
|
|
2
|
+
import { withTransientRetry } from "./backoff.js";
|
|
3
|
+
export class OpenRouterLlmClient {
|
|
4
|
+
baseUrl;
|
|
5
|
+
apiKey;
|
|
6
|
+
referer;
|
|
7
|
+
title;
|
|
8
|
+
fetchImpl;
|
|
9
|
+
retry;
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
this.baseUrl = (opts.baseUrl ?? "https://openrouter.ai/api/v1").replace(/\/$/, "");
|
|
12
|
+
this.apiKey = opts.apiKey;
|
|
13
|
+
this.referer = opts.referer;
|
|
14
|
+
this.title = opts.title;
|
|
15
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
16
|
+
this.retry = opts.retry ?? {};
|
|
17
|
+
}
|
|
18
|
+
async chatCompletion(params) {
|
|
19
|
+
const body = {
|
|
20
|
+
model: params.model,
|
|
21
|
+
messages: params.messages,
|
|
22
|
+
usage: { include: true },
|
|
23
|
+
...(params.temperature != null ? { temperature: params.temperature } : {}),
|
|
24
|
+
...(params.max_tokens != null ? { max_tokens: params.max_tokens } : {}),
|
|
25
|
+
...(params.reasoning != null ? { reasoning: params.reasoning } : {}),
|
|
26
|
+
...(params.tools && params.tools.length > 0 ? { tools: params.tools } : {}),
|
|
27
|
+
};
|
|
28
|
+
const headers = {
|
|
29
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
};
|
|
32
|
+
if (this.referer)
|
|
33
|
+
headers["HTTP-Referer"] = this.referer;
|
|
34
|
+
if (this.title)
|
|
35
|
+
headers["X-Title"] = this.title;
|
|
36
|
+
return withTransientRetry(async () => {
|
|
37
|
+
const res = await this.fetchImpl(`${this.baseUrl}/chat/completions`, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers,
|
|
40
|
+
body: JSON.stringify(body),
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok)
|
|
43
|
+
throw await providerErrorFromResponse(res, "Provider LLM");
|
|
44
|
+
const payload = await res.json();
|
|
45
|
+
const embedded = errorInBody(payload, "Provider LLM");
|
|
46
|
+
if (embedded)
|
|
47
|
+
throw embedded;
|
|
48
|
+
const completion = payload;
|
|
49
|
+
const choice = completion.choices?.[0];
|
|
50
|
+
const empty = emptyCompletionError({
|
|
51
|
+
model: completion.model ?? params.model,
|
|
52
|
+
finishReason: choice?.finish_reason,
|
|
53
|
+
content: choice?.message.content,
|
|
54
|
+
toolCalls: choice?.message.tool_calls,
|
|
55
|
+
maxTokens: params.max_tokens ?? null,
|
|
56
|
+
completionTokens: completion.usage?.completion_tokens ?? 0,
|
|
57
|
+
reasoningTokens: completion.usage?.completion_tokens_details?.reasoning_tokens ?? 0,
|
|
58
|
+
});
|
|
59
|
+
if (empty)
|
|
60
|
+
throw empty;
|
|
61
|
+
return completion;
|
|
62
|
+
}, this.retry);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function createLlmClient(opts) {
|
|
66
|
+
return new OpenRouterLlmClient({
|
|
67
|
+
apiKey: opts.providerApiKey,
|
|
68
|
+
baseUrl: opts.baseUrl,
|
|
69
|
+
referer: "https://fantastico.cc",
|
|
70
|
+
title: "FANTASTICO SDK",
|
|
71
|
+
fetchImpl: opts.fetchImpl,
|
|
72
|
+
retry: opts.retry,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ToolDefinition } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Tool locale: la dichiarazione (nome/descrizione/schema) va al modello, `execute` resta
|
|
4
|
+
* nel processo dell'integratore e non è mai visibile al modello. Riceve gli argomenti
|
|
5
|
+
* generati dal modello più il `context` fidato dell'istanza/chiamata (mai generato dal
|
|
6
|
+
* modello: mai fidarsi di un identificatore che arriva dal modello).
|
|
7
|
+
*/
|
|
8
|
+
export interface LocalToolDefinition<TContext = unknown> {
|
|
9
|
+
type: "function";
|
|
10
|
+
function: ToolDefinition["function"];
|
|
11
|
+
execute: (args: Record<string, unknown>, context: TContext) => unknown | Promise<unknown>;
|
|
12
|
+
/** true = non gira in batch con altri tool dello stesso turno (default: parallelo). */
|
|
13
|
+
sequential?: boolean;
|
|
14
|
+
/** true = il modello può proporlo ma non esegue finché il chiamante non conferma. */
|
|
15
|
+
requiresConfirmation?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Frase umana da mostrare all'operatore quando `requiresConfirmation` ferma questa
|
|
18
|
+
* tool_call (es. `(args) => \`Trasferisci $${args.amount} al conto "${args.userKey}"\``).
|
|
19
|
+
* Ignorata se `requiresConfirmation` non è true. Senza, il chiamante mostra nome+argomenti
|
|
20
|
+
* grezzi — utilizzabile, ma non pensato per un umano.
|
|
21
|
+
*/
|
|
22
|
+
confirmationSummary?: (args: Record<string, unknown>) => string;
|
|
23
|
+
/**
|
|
24
|
+
* Frase breve per chi guarda il turno mentre il tool gira (es. "Leggo il contratto…"),
|
|
25
|
+
* consegnata nell'evento `tool_start` (`onEvent`, `activity.ts`). Senza, il chiamante
|
|
26
|
+
* ripiega sul nome del tool.
|
|
27
|
+
*/
|
|
28
|
+
activityLabel?: (args: Record<string, unknown>) => string;
|
|
29
|
+
/** Timeout esecuzione in ms (default 10_000). */
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
/** Filtro di disponibilità per turno; false = il tool non è offerto al modello ora. */
|
|
32
|
+
available?: (context: TContext) => boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Tool "azione client": il server non lo esegue mai. Il modello lo propone, l'orchestratore
|
|
36
|
+
* lo consegna così com'è al chiamante (frontend), che decide come compierlo — es. navigare,
|
|
37
|
+
* aprire un pannello. Simmetrico a LocalToolDefinition ma senza `execute` lato server.
|
|
38
|
+
*/
|
|
39
|
+
export interface ClientActionToolDefinition<TContext = unknown> {
|
|
40
|
+
type: "function";
|
|
41
|
+
function: ToolDefinition["function"];
|
|
42
|
+
clientAction: true;
|
|
43
|
+
/**
|
|
44
|
+
* true = il loop si ferma e aspetta l'esito dal client prima di proseguire.
|
|
45
|
+
* false (default) = fire-and-forget: il loop prosegue subito, l'azione viene solo
|
|
46
|
+
* raccolta e restituita nel risultato finale (es. "naviga qui", nessuna risposta attesa).
|
|
47
|
+
*/
|
|
48
|
+
awaitsResult?: boolean;
|
|
49
|
+
sequential?: boolean;
|
|
50
|
+
available?: (context: TContext) => boolean;
|
|
51
|
+
}
|
|
52
|
+
export type RegisteredTool<TContext = unknown> = LocalToolDefinition<TContext> | ClientActionToolDefinition<TContext>;
|
|
53
|
+
export declare function isClientAction<TContext>(tool: RegisteredTool<TContext>): tool is ClientActionToolDefinition<TContext>;
|
|
54
|
+
/**
|
|
55
|
+
* Registro dei tool locali/azione-client di un'istanza. Non esegue mai un'azione client
|
|
56
|
+
* (non ha modo di farlo: gira nel processo dell'integratore, non nel browser) — solo
|
|
57
|
+
* `execute()` sui tool locali veri, sempre isolato (mai lancia) e mai ritentato in automatico
|
|
58
|
+
* (una funzione dell'integratore può non essere idempotente).
|
|
59
|
+
*/
|
|
60
|
+
export declare class LocalToolRegistry<TContext = unknown> {
|
|
61
|
+
private readonly tools;
|
|
62
|
+
constructor(initial?: RegisteredTool<TContext>[]);
|
|
63
|
+
add(tool: RegisteredTool<TContext>): void;
|
|
64
|
+
/** Un registro nuovo: questi tool più `extra`, che vince sullo stesso nome. Questo non cambia. */
|
|
65
|
+
with(extra: RegisteredTool<TContext>[]): LocalToolRegistry<TContext>;
|
|
66
|
+
get(name: string): RegisteredTool<TContext> | undefined;
|
|
67
|
+
/** Definizioni offerte al modello per QUESTO turno: filtra per `available(context)`. */
|
|
68
|
+
definitionsFor(context: TContext): ToolDefinition[];
|
|
69
|
+
/** Esegue un tool locale (mai un'azione client, mai un tool sconosciuto). Non lancia mai:
|
|
70
|
+
* un fallimento diventa un tool_result di errore che il modello vede e su cui reagisce. */
|
|
71
|
+
execute(name: string, args: Record<string, unknown>, context: TContext): Promise<string>;
|
|
72
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export function isClientAction(tool) {
|
|
2
|
+
return tool.clientAction === true;
|
|
3
|
+
}
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
5
|
+
function withTimeout(p, ms) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const timer = setTimeout(() => reject(new Error(`Timeout dopo ${ms}ms`)), ms);
|
|
8
|
+
p.then((v) => {
|
|
9
|
+
clearTimeout(timer);
|
|
10
|
+
resolve(v);
|
|
11
|
+
}, (e) => {
|
|
12
|
+
clearTimeout(timer);
|
|
13
|
+
reject(e);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export class LocalToolRegistry {
|
|
18
|
+
tools = new Map();
|
|
19
|
+
constructor(initial = []) {
|
|
20
|
+
for (const t of initial)
|
|
21
|
+
this.add(t);
|
|
22
|
+
}
|
|
23
|
+
add(tool) {
|
|
24
|
+
this.tools.set(tool.function.name, tool);
|
|
25
|
+
}
|
|
26
|
+
with(extra) {
|
|
27
|
+
return new LocalToolRegistry([...this.tools.values(), ...extra]);
|
|
28
|
+
}
|
|
29
|
+
get(name) {
|
|
30
|
+
return this.tools.get(name);
|
|
31
|
+
}
|
|
32
|
+
definitionsFor(context) {
|
|
33
|
+
const defs = [];
|
|
34
|
+
for (const tool of this.tools.values()) {
|
|
35
|
+
if (tool.available && !tool.available(context))
|
|
36
|
+
continue;
|
|
37
|
+
defs.push({ type: "function", function: tool.function });
|
|
38
|
+
}
|
|
39
|
+
return defs;
|
|
40
|
+
}
|
|
41
|
+
async execute(name, args, context) {
|
|
42
|
+
const tool = this.tools.get(name);
|
|
43
|
+
if (!tool || isClientAction(tool)) {
|
|
44
|
+
return JSON.stringify({ error: `Tool locale sconosciuto: ${name}` });
|
|
45
|
+
}
|
|
46
|
+
const timeoutMs = tool.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
47
|
+
try {
|
|
48
|
+
const result = await withTimeout(Promise.resolve(tool.execute(args, context)), timeoutMs);
|
|
49
|
+
return typeof result === "string" ? result : JSON.stringify(result ?? null);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
return JSON.stringify({ error: err.message ?? "Errore tool locale" });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quanti token di risposta si possono chiedere con la runtime key che si ha in mano.
|
|
3
|
+
*
|
|
4
|
+
* **Il problema che risolve.** OpenRouter accetta o rifiuta una richiesta confrontando il costo del
|
|
5
|
+
* CASO PEGGIORE — `max_tokens` moltiplicato per il prezzo in uscita del modello — con quel che resta
|
|
6
|
+
* sulla chiave, *prima* di generare qualunque cosa. Omesso, `max_tokens` vale il massimo del
|
|
7
|
+
* modello: decine di migliaia di token. Una chiave coniata su un `block` piccolo viene quindi
|
|
8
|
+
* rifiutata anche quando la risposta vera costerebbe un millesimo di dollaro — e il messaggio parla
|
|
9
|
+
* di `max_tokens`, che è l'ultimo posto in cui si va a cercare un problema di credito.
|
|
10
|
+
*
|
|
11
|
+
* È esattamente il conto che `scripts/try-inference.mjs` fa a mano da sempre. Farlo lì e non qui
|
|
12
|
+
* voleva dire che lo script funzionava e il prodotto no.
|
|
13
|
+
*
|
|
14
|
+
* **Non è un tetto imposto al chiamante.** Un `max_tokens` esplicito passa intatto: chi lo scrive sa
|
|
15
|
+
* cosa vuole, e se non entra nel credito è giusto che OpenRouter glielo dica. Questo vale solo per
|
|
16
|
+
* il caso in cui nessuno ha detto niente.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* `undefined` = non scrivere `max_tokens`, che è anche la risposta quando manca un dato: senza il
|
|
20
|
+
* tetto della chiave o senza il prezzo del modello non c'è niente da calcolare, e **indovinare
|
|
21
|
+
* sarebbe peggio che tacere** — un numero inventato romperebbe le chiavi capienti per riparare
|
|
22
|
+
* quelle piccole.
|
|
23
|
+
*
|
|
24
|
+
* @param keyLimitUsd il tetto della runtime key, in USD (dal conio: `limit`)
|
|
25
|
+
* @param pricePerOutputToken USD per token in uscita (dal catalogo: `pricing.completion`)
|
|
26
|
+
*/
|
|
27
|
+
export declare function affordableMaxTokens(keyLimitUsd: number | null | undefined, pricePerOutputToken: number | null | undefined): number | undefined;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const HEADROOM = 0.8;
|
|
2
|
+
const COMFORTABLE = 8192;
|
|
3
|
+
const FLOOR = 64;
|
|
4
|
+
export function affordableMaxTokens(keyLimitUsd, pricePerOutputToken) {
|
|
5
|
+
if (keyLimitUsd == null || !Number.isFinite(keyLimitUsd) || keyLimitUsd <= 0)
|
|
6
|
+
return undefined;
|
|
7
|
+
if (pricePerOutputToken == null
|
|
8
|
+
|| !Number.isFinite(pricePerOutputToken)
|
|
9
|
+
|| pricePerOutputToken <= 0) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const affordable = Math.floor((keyLimitUsd * HEADROOM) / pricePerOutputToken);
|
|
13
|
+
if (affordable >= COMFORTABLE)
|
|
14
|
+
return undefined;
|
|
15
|
+
return Math.max(FLOOR, affordable);
|
|
16
|
+
}
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { McpExecutor } from "./orchestrator.js";
|
|
2
|
+
export interface McpExecutorOptions {
|
|
3
|
+
timeoutMs?: number;
|
|
4
|
+
maxRetries?: number;
|
|
5
|
+
fetchImpl?: typeof fetch;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Crea un McpExecutor: chiama **il tool**, al suo endpoint, col token emesso per lui.
|
|
9
|
+
*
|
|
10
|
+
* `tool.url` e `<endpoint>/execute` e `tool.authToken` e il tool token della concessione —
|
|
11
|
+
* entrambi arrivano dal conio e li compone `AuthClient.describeTool`.
|
|
12
|
+
* Payload: { tool_id, name, arguments } → risultato JSON.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createMcpExecutor(opts?: McpExecutorOptions): McpExecutor;
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
function isRetriable(status, err) {
|
|
2
|
+
if (err?.name === "AbortError" || err?.name === "TimeoutError")
|
|
3
|
+
return false;
|
|
4
|
+
const code = err?.cause?.code;
|
|
5
|
+
if (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "EAI_AGAIN")
|
|
6
|
+
return true;
|
|
7
|
+
if (status === 429 || status === 503)
|
|
8
|
+
return true;
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
function sleep(ms) {
|
|
12
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
13
|
+
}
|
|
14
|
+
export function createMcpExecutor(opts = {}) {
|
|
15
|
+
const timeoutMs = opts.timeoutMs ?? 180_000;
|
|
16
|
+
const maxRetries = opts.maxRetries ?? 2;
|
|
17
|
+
const fetchFn = opts.fetchImpl ?? fetch;
|
|
18
|
+
return async (tool, toolName, args) => {
|
|
19
|
+
const body = JSON.stringify({ tool_id: tool.name, name: toolName, arguments: args });
|
|
20
|
+
let lastErr;
|
|
21
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetchFn(tool.url, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${tool.authToken}` },
|
|
28
|
+
body,
|
|
29
|
+
signal: controller.signal,
|
|
30
|
+
});
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
if (res.ok) {
|
|
33
|
+
const json = await res.json();
|
|
34
|
+
return typeof json === "string" ? json : JSON.stringify(json);
|
|
35
|
+
}
|
|
36
|
+
if (!isRetriable(res.status))
|
|
37
|
+
throw new Error(`MCP ${tool.name}/${toolName}: ${res.status}`);
|
|
38
|
+
lastErr = new Error(`MCP ${res.status}`);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
lastErr = err;
|
|
43
|
+
if (lastErr.name === "AbortError") {
|
|
44
|
+
lastErr = new Error(`MCP ${tool.name}/${toolName}: nessuna risposta entro ${Math.round(timeoutMs / 1000)} s`);
|
|
45
|
+
throw lastErr;
|
|
46
|
+
}
|
|
47
|
+
if (!isRetriable(undefined, lastErr))
|
|
48
|
+
throw lastErr;
|
|
49
|
+
}
|
|
50
|
+
if (attempt < maxRetries)
|
|
51
|
+
await sleep(1000 * Math.pow(2, attempt));
|
|
52
|
+
}
|
|
53
|
+
throw lastErr ?? new Error(`MCP ${tool.name}/${toolName} failed`);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ToolConfig, ToolDefinition, ToolCall, ChatCompletion, PendingCompletion, ResumeDecision, ClientAction, ReasoningConfig } from "./types.js";
|
|
2
|
+
import { LocalToolRegistry } from "./local-tools.js";
|
|
3
|
+
import { type ChatActivityListener } from "./activity.js";
|
|
4
|
+
export interface InternalMessage {
|
|
5
|
+
role: "system" | "user" | "assistant" | "tool";
|
|
6
|
+
content?: string | null;
|
|
7
|
+
tool_calls?: ToolCall[];
|
|
8
|
+
tool_call_id?: string;
|
|
9
|
+
}
|
|
10
|
+
export type { ToolCall };
|
|
11
|
+
export interface LlmResponse {
|
|
12
|
+
id: string;
|
|
13
|
+
model: string;
|
|
14
|
+
choices: Array<{
|
|
15
|
+
message: {
|
|
16
|
+
role: "assistant";
|
|
17
|
+
content?: string | null;
|
|
18
|
+
tool_calls?: ToolCall[];
|
|
19
|
+
};
|
|
20
|
+
finish_reason: string;
|
|
21
|
+
}>;
|
|
22
|
+
usage?: {
|
|
23
|
+
prompt_tokens?: number;
|
|
24
|
+
completion_tokens?: number;
|
|
25
|
+
total_tokens?: number;
|
|
26
|
+
cost?: number;
|
|
27
|
+
prompt_tokens_details?: {
|
|
28
|
+
cached_tokens?: number;
|
|
29
|
+
cache_write_tokens?: number;
|
|
30
|
+
};
|
|
31
|
+
completion_tokens_details?: {
|
|
32
|
+
/** Token di ragionamento: gia' compresi in `completion_tokens`, non da sommare. */
|
|
33
|
+
reasoning_tokens?: number;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export interface LlmClient {
|
|
38
|
+
chatCompletion(params: {
|
|
39
|
+
model: string;
|
|
40
|
+
messages: InternalMessage[];
|
|
41
|
+
tools?: ToolDefinition[];
|
|
42
|
+
temperature?: number;
|
|
43
|
+
max_tokens?: number;
|
|
44
|
+
reasoning?: ReasoningConfig;
|
|
45
|
+
}): Promise<LlmResponse>;
|
|
46
|
+
}
|
|
47
|
+
export type McpExecutor = (tool: ToolConfig, toolName: string, args: Record<string, unknown>) => Promise<string>;
|
|
48
|
+
export interface OrchestrateOptions {
|
|
49
|
+
model: string;
|
|
50
|
+
remoteTools: ToolConfig[];
|
|
51
|
+
localTools: LocalToolRegistry;
|
|
52
|
+
/** Contesto fidato per questa chiamata, passato a ogni execute() di tool locale. */
|
|
53
|
+
context: unknown;
|
|
54
|
+
mcpExecutor: McpExecutor;
|
|
55
|
+
maxTurns?: number;
|
|
56
|
+
budgetMs?: number;
|
|
57
|
+
temperature?: number;
|
|
58
|
+
maxTokens?: number;
|
|
59
|
+
/** Passato identico a ogni turno. Omesso, non finisce nel corpo. */
|
|
60
|
+
reasoning?: ReasoningConfig;
|
|
61
|
+
/** Eventi del turno mentre accadono (vedi `activity.ts`). */
|
|
62
|
+
onEvent?: ChatActivityListener;
|
|
63
|
+
}
|
|
64
|
+
/** Stato serializzabile della conversazione: quello che sopravvive a una pausa/resume. */
|
|
65
|
+
export interface OrchestrationState {
|
|
66
|
+
conv: InternalMessage[];
|
|
67
|
+
turns: number;
|
|
68
|
+
mcpCalls: number;
|
|
69
|
+
promptTokens: number;
|
|
70
|
+
completionTokens: number;
|
|
71
|
+
cacheCreationTokens: number;
|
|
72
|
+
cacheReadTokens: number;
|
|
73
|
+
/**
|
|
74
|
+
* Opzionale perche' `resumeOrchestration` fa un cast, non una convalida: uno stato messo in
|
|
75
|
+
* pausa da una versione che non aveva questo campo si riprende con la nuova, e sommare a
|
|
76
|
+
* `undefined` darebbe un `NaN` che si propaga fino all'usage del chiamante.
|
|
77
|
+
*/
|
|
78
|
+
reasoningTokens?: number;
|
|
79
|
+
costUsd: number;
|
|
80
|
+
clientActions: ClientAction[];
|
|
81
|
+
budgetMs: number;
|
|
82
|
+
maxTurns: number;
|
|
83
|
+
model: string;
|
|
84
|
+
}
|
|
85
|
+
export type OrchestrateResult = ChatCompletion | PendingCompletion;
|
|
86
|
+
export declare class OrchestrationError extends Error {
|
|
87
|
+
readonly code: "TIMEOUT" | "UPSTREAM_ERROR" | "INVALID_STATE";
|
|
88
|
+
constructor(message: string, code: "TIMEOUT" | "UPSTREAM_ERROR" | "INVALID_STATE");
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Il tetto di tempo di un'orchestrazione. Esportato perché serve anche a `client.ts`, che da esso
|
|
92
|
+
* ricava la scadenza entro cui un backoff può dormire: due default scritti in due posti
|
|
93
|
+
* divergerebbero, e il sintomo sarebbe un'attesa che sfora un budget che credeva di rispettare.
|
|
94
|
+
*/
|
|
95
|
+
export declare const DEFAULT_BUDGET_MS = 90000;
|
|
96
|
+
export declare function orchestrate(messages: InternalMessage[], llm: LlmClient, opts: OrchestrateOptions): Promise<OrchestrateResult>;
|
|
97
|
+
/**
|
|
98
|
+
* Riprende un'orchestrazione fermata da `orchestrate()`/`resumeOrchestration()` con
|
|
99
|
+
* `status: "pending"`. Ogni tool_call sospesa con `reason` deve avere una decisione
|
|
100
|
+
* corrispondente in `decisions`; se manca, viene trattata come negata/fallita (mai lasciata
|
|
101
|
+
* a metà: un tool_call senza tool_result rende la conversazione malformata per l'API).
|
|
102
|
+
* Le tool_call sospese SENZA `reason` (erano solo nello stesso turno di una bloccante)
|
|
103
|
+
* vengono eseguite in automatico, ignorando eventuali decisioni passate per loro.
|
|
104
|
+
*/
|
|
105
|
+
export declare function resumeOrchestration(rawState: unknown, decisions: ResumeDecision[], llm: LlmClient, opts: OrchestrateOptions): Promise<OrchestrateResult>;
|