@agent-native/core 0.204.0 → 0.204.1-nightly-20261008034100
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/dist/action.js +5 -6
- package/dist/agent/auto-continue.d.ts +35 -0
- package/dist/agent/auto-continue.js +38 -0
- package/dist/agent/connection-required-note.d.ts +52 -0
- package/dist/agent/connection-required-note.js +187 -0
- package/dist/agent/jev-tool-prefetch.d.ts +5 -1
- package/dist/agent/jev-tool-prefetch.js +19 -6
- package/dist/agent/previous-turn-note.d.ts +9 -0
- package/dist/agent/previous-turn-note.js +48 -0
- package/dist/agent/production-agent.d.ts +17 -13
- package/dist/agent/production-agent.js +185 -43
- package/dist/agent/reaped-run-thread.d.ts +13 -0
- package/dist/agent/reaped-run-thread.js +45 -0
- package/dist/agent/run-store.d.ts +29 -5
- package/dist/agent/run-store.js +144 -30
- package/dist/agent/thread-data-builder.d.ts +16 -0
- package/dist/agent/thread-data-builder.js +30 -8
- package/dist/agent/thread-message-projection.js +5 -3
- package/dist/agent/tool-search.js +5 -1
- package/dist/agent/types.d.ts +3 -0
- package/dist/audit/config.d.ts +6 -0
- package/dist/audit/config.js +12 -1
- package/dist/audit/index.d.ts +1 -1
- package/dist/audit/index.js +1 -1
- package/dist/audit/store.js +39 -4
- package/dist/cli/skills.d.ts +1 -0
- package/dist/cli/skills.js +1 -1
- package/dist/client/chat/agentkit-agent-native.js +83 -7
- package/dist/client/chat/agentkit-protocol.js +140 -67
- package/dist/client/chat/runtime.js +5 -3
- package/dist/client/use-run-stuck-detection.d.ts +18 -1
- package/dist/client/use-run-stuck-detection.js +115 -75
- package/dist/db/request-telemetry.d.ts +7 -1
- package/dist/db/request-telemetry.js +20 -1
- package/dist/deploy/build.d.ts +25 -1
- package/dist/deploy/build.js +176 -3
- package/dist/deploy/workspace-deploy.js +8 -3
- package/dist/extensions/fetch-tool.js +50 -14
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/integrations/adapters/email.js +11 -3
- package/dist/integrations/adapters/telegram.js +4 -2
- package/dist/jobs/actions/automation-no-op.d.ts +12 -0
- package/dist/jobs/actions/automation-no-op.js +20 -0
- package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
- package/dist/jobs/background-automation-runner.d.ts +8 -1
- package/dist/jobs/background-automation-runner.js +102 -19
- package/dist/jobs/run-history.d.ts +1 -1
- package/dist/jobs/run-history.js +8 -4
- package/dist/jobs/scheduler.js +25 -12
- package/dist/localization/automation-outcome-messages.d.ts +22 -0
- package/dist/localization/automation-outcome-messages.js +90 -0
- package/dist/localization/default-messages.d.ts +2 -0
- package/dist/localization/default-messages.js +4 -2
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/metrics.d.ts +3 -0
- package/dist/observability/metrics.js +10 -0
- package/dist/observability/migrations.js +22 -0
- package/dist/observability/routes.d.ts +13 -8
- package/dist/observability/routes.js +88 -14
- package/dist/observability/sentiment.d.ts +5 -0
- package/dist/observability/sentiment.js +75 -25
- package/dist/observability/store.d.ts +28 -0
- package/dist/observability/store.js +58 -0
- package/dist/observability/traces.js +37 -5
- package/dist/org/index.d.ts +2 -2
- package/dist/progress/actions.js +1 -0
- package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
- package/dist/provider-api/actions/provider-api.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.js +6 -2
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/provider-api/index.d.ts +1 -0
- package/dist/provider-api/index.js +18 -0
- package/dist/provider-api/result-outcome.d.ts +1 -0
- package/dist/provider-api/result-outcome.js +20 -0
- package/dist/provider-api/staging.js +10 -9
- package/dist/server/agent-chat/prompt-resources.d.ts +8 -2
- package/dist/server/agent-chat/prompt-resources.js +30 -7
- package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +19 -4
- package/dist/server/agent-chat/recurring-jobs-runtime.js +52 -16
- package/dist/server/agent-chat/sweep-trigger-auth.d.ts +18 -0
- package/dist/server/agent-chat/sweep-trigger-auth.js +36 -0
- package/dist/server/agent-chat-plugin.js +20 -13
- package/dist/server/agent-engine-default-model-route.d.ts +2 -2
- package/dist/server/auth.js +4 -4
- package/dist/server/framework-request-handler.js +19 -0
- package/dist/server/http-response-telemetry.js +4 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/request-context.d.ts +6 -0
- package/dist/shared/context-status.d.ts +21 -0
- package/dist/shared/context-status.js +54 -0
- package/dist/templates/chat/app/components/chat/ChatRouteContent.tsx +8 -1
- package/dist/templates/workspace-core/.agents/skills/automations/SKILL.md +8 -1
- package/dist/templates/workspace-core/.agents/skills/recurring-jobs/SKILL.md +3 -1
- package/dist/triggers/actions/manage-automation.d.ts +7 -7
- package/dist/triggers/dispatcher.js +10 -7
- package/docs/content/audit-log.mdx +3 -3
- package/docs/content/cloudflare.mdx +27 -0
- package/docs/content/deployment-environment-variables.mdx +2 -0
- package/docs/content/durable-resume.mdx +3 -1
- package/docs/content/environment-variables.mdx +2 -0
- package/docs/content/recurring-jobs.mdx +29 -9
- package/docs/content/vercel.mdx +30 -0
- package/package.json +3 -3
- package/src/templates/chat/app/components/chat/ChatRouteContent.tsx +8 -1
- package/src/templates/workspace-core/.agents/skills/automations/SKILL.md +8 -1
- package/src/templates/workspace-core/.agents/skills/recurring-jobs/SKILL.md +3 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type BuiltinLocaleCode } from "./shared.js";
|
|
2
|
+
export declare const AUTOMATION_OUTCOME_MESSAGES: Record<BuiltinLocaleCode, {
|
|
3
|
+
noWork: string;
|
|
4
|
+
emptyDelivery: string;
|
|
5
|
+
emailNotSent: string;
|
|
6
|
+
noOpInstruction: string;
|
|
7
|
+
noOpReason: string;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function automationOutcomeMessagesForLocale(locale: string): {
|
|
10
|
+
noWork: string;
|
|
11
|
+
emptyDelivery: string;
|
|
12
|
+
emailNotSent: string;
|
|
13
|
+
noOpInstruction: string;
|
|
14
|
+
noOpReason: string;
|
|
15
|
+
};
|
|
16
|
+
export declare function automationOutcomeMessagesForUser(userEmail: string | null | undefined): Promise<{
|
|
17
|
+
noWork: string;
|
|
18
|
+
emptyDelivery: string;
|
|
19
|
+
emailNotSent: string;
|
|
20
|
+
noOpInstruction: string;
|
|
21
|
+
noOpReason: string;
|
|
22
|
+
}>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { getUserSetting } from "../settings/user-settings.js";
|
|
2
|
+
import { DEFAULT_LOCALE, isLocaleCode, LOCALIZATION_SETTING_KEY, normalizeLocalizationPreference, } from "./shared.js";
|
|
3
|
+
export const AUTOMATION_OUTCOME_MESSAGES = {
|
|
4
|
+
"en-US": {
|
|
5
|
+
noOpInstruction: "If nothing needs doing, call automation-no-op with a short reason. Failed tools, sends, or missing credentials are errors, not no-ops.",
|
|
6
|
+
noOpReason: "A short reason why no action or report was needed (1–500 characters).",
|
|
7
|
+
emailNotSent: "The email was not sent by a delivery provider.",
|
|
8
|
+
noWork: "The automation ended without a confirmed action. Configure a delivery destination for read-only reports.",
|
|
9
|
+
emptyDelivery: "The automation produced no message for its configured delivery destination.",
|
|
10
|
+
},
|
|
11
|
+
"es-ES": {
|
|
12
|
+
noOpInstruction: "Si no hay nada que hacer, llama a automation-no-op con un motivo breve. Las herramientas o los envíos fallidos y las credenciales ausentes son errores, no casos sin trabajo.",
|
|
13
|
+
noOpReason: "Motivo breve por el que no se necesitaba ninguna acción ni informe (1–500 caracteres).",
|
|
14
|
+
emailNotSent: "El correo no fue enviado por un proveedor de entrega.",
|
|
15
|
+
noWork: "La automatización terminó sin una acción confirmada. Configura un destino de entrega para los informes de solo lectura.",
|
|
16
|
+
emptyDelivery: "La automatización no produjo ningún mensaje para su destino de entrega configurado.",
|
|
17
|
+
},
|
|
18
|
+
"fr-FR": {
|
|
19
|
+
noOpInstruction: "Si rien n’est à faire, appelez automation-no-op avec une courte raison. Les échecs d’outils ou d’envoi et les identifiants manquants sont des erreurs, pas une absence de travail.",
|
|
20
|
+
noOpReason: "Courte raison pour laquelle aucune action ni aucun rapport n’était nécessaire (1–500 caractères).",
|
|
21
|
+
emailNotSent: "L’e-mail n’a pas été envoyé par un fournisseur de messagerie.",
|
|
22
|
+
noWork: "L’automatisation s’est terminée sans action confirmée. Configurez une destination de livraison pour les rapports en lecture seule.",
|
|
23
|
+
emptyDelivery: "L’automatisation n’a produit aucun message pour sa destination de livraison configurée.",
|
|
24
|
+
},
|
|
25
|
+
"de-DE": {
|
|
26
|
+
noOpInstruction: "Wenn nichts zu tun ist, rufen Sie automation-no-op mit einer kurzen Begründung auf. Fehlgeschlagene Tools oder Zustellungen und fehlende Zugangsdaten sind Fehler, keine Fälle ohne Handlungsbedarf.",
|
|
27
|
+
noOpReason: "Kurze Begründung, warum keine Aktion oder kein Bericht nötig war (1–500 Zeichen).",
|
|
28
|
+
emailNotSent: "Die E-Mail wurde von keinem Zustellanbieter versendet.",
|
|
29
|
+
noWork: "Die Automatisierung endete ohne bestätigte Aktion. Konfigurieren Sie ein Zustellungsziel für schreibgeschützte Berichte.",
|
|
30
|
+
emptyDelivery: "Die Automatisierung hat keine Nachricht für ihr konfiguriertes Zustellungsziel erzeugt.",
|
|
31
|
+
},
|
|
32
|
+
"pt-BR": {
|
|
33
|
+
noOpInstruction: "Se não houver nada a fazer, chame automation-no-op com um motivo breve. Falhas de ferramentas ou envios e credenciais ausentes são erros, não ausência de trabalho.",
|
|
34
|
+
noOpReason: "Motivo breve pelo qual nenhuma ação ou relatório era necessário (1–500 caracteres).",
|
|
35
|
+
emailNotSent: "O e-mail não foi enviado por um provedor de entrega.",
|
|
36
|
+
noWork: "A automação terminou sem uma ação confirmada. Configure um destino de entrega para relatórios somente de leitura.",
|
|
37
|
+
emptyDelivery: "A automação não produziu nenhuma mensagem para seu destino de entrega configurado.",
|
|
38
|
+
},
|
|
39
|
+
"zh-CN": {
|
|
40
|
+
noOpInstruction: "如果没有需要执行的工作,请调用 automation-no-op 并提供简短原因。工具失败、发送失败或缺少凭据属于错误,不是无需操作。",
|
|
41
|
+
noOpReason: "简要说明为何无需执行操作或发送报告(1–500 个字符)。",
|
|
42
|
+
emailNotSent: "电子邮件未通过邮件服务提供商发送。",
|
|
43
|
+
noWork: "自动化结束时没有已确认的操作。请为只读报告配置发送目标。",
|
|
44
|
+
emptyDelivery: "自动化未生成要发送到已配置目标的消息。",
|
|
45
|
+
},
|
|
46
|
+
"zh-TW": {
|
|
47
|
+
noOpInstruction: "如果沒有需要執行的工作,請呼叫 automation-no-op 並提供簡短原因。工具失敗、傳送失敗或缺少憑證屬於錯誤,不是無需操作。",
|
|
48
|
+
noOpReason: "簡要說明為何無需執行操作或傳送報告(1–500 個字元)。",
|
|
49
|
+
emailNotSent: "電子郵件未透過郵件服務供應商傳送。",
|
|
50
|
+
noWork: "自動化結束時沒有已確認的操作。請為唯讀報告設定傳送目標。",
|
|
51
|
+
emptyDelivery: "自動化未產生要傳送到已設定目標的訊息。",
|
|
52
|
+
},
|
|
53
|
+
"ja-JP": {
|
|
54
|
+
noOpInstruction: "対応が不要な場合は、短い理由を添えて automation-no-op を呼び出してください。ツールや送信の失敗、認証情報の不足はエラーであり、対応不要には該当しません。",
|
|
55
|
+
noOpReason: "操作やレポートが不要だった短い理由(1~500文字)。",
|
|
56
|
+
emailNotSent: "メールは配信プロバイダーから送信されませんでした。",
|
|
57
|
+
noWork: "自動化は操作が確認されないまま終了しました。読み取り専用のレポートには配信先を設定してください。",
|
|
58
|
+
emptyDelivery: "自動化は設定された配信先へのメッセージを生成しませんでした。",
|
|
59
|
+
},
|
|
60
|
+
"ko-KR": {
|
|
61
|
+
noOpInstruction: "할 일이 없으면 짧은 이유와 함께 automation-no-op을 호출하세요. 도구 또는 전송 실패와 자격 증명 누락은 작업 없음이 아닌 오류입니다.",
|
|
62
|
+
noOpReason: "작업이나 보고서가 필요하지 않았던 짧은 이유(1–500자).",
|
|
63
|
+
emailNotSent: "이메일이 전송 제공업체를 통해 전송되지 않았습니다.",
|
|
64
|
+
noWork: "확인된 작업 없이 자동화가 종료되었습니다. 읽기 전용 보고서에는 전송 대상을 설정하세요.",
|
|
65
|
+
emptyDelivery: "자동화가 설정된 전송 대상에 보낼 메시지를 생성하지 않았습니다.",
|
|
66
|
+
},
|
|
67
|
+
"hi-IN": {
|
|
68
|
+
noOpInstruction: "यदि कुछ करने की आवश्यकता नहीं है, तो संक्षिप्त कारण के साथ automation-no-op कॉल करें। विफल टूल, विफल भेजने की कोशिश या अनुपलब्ध क्रेडेंशियल त्रुटियाँ हैं, काम न होने की स्थिति नहीं।",
|
|
69
|
+
noOpReason: "किसी कार्रवाई या रिपोर्ट की आवश्यकता न होने का संक्षिप्त कारण (1–500 अक्षर)।",
|
|
70
|
+
emailNotSent: "ईमेल किसी डिलीवरी प्रदाता द्वारा नहीं भेजा गया।",
|
|
71
|
+
noWork: "ऑटोमेशन किसी पुष्ट कार्रवाई के बिना समाप्त हो गया। केवल पढ़ने वाली रिपोर्ट के लिए डिलीवरी गंतव्य कॉन्फ़िगर करें।",
|
|
72
|
+
emptyDelivery: "ऑटोमेशन ने अपने कॉन्फ़िगर किए गए डिलीवरी गंतव्य के लिए कोई संदेश नहीं बनाया।",
|
|
73
|
+
},
|
|
74
|
+
"ar-SA": {
|
|
75
|
+
noOpInstruction: "إذا لم يكن هناك ما يجب فعله، استدعِ automation-no-op مع سبب موجز. فشل الأدوات أو الإرسال أو غياب بيانات الاعتماد أخطاء، وليس حالات لا تتطلب عملاً.",
|
|
76
|
+
noOpReason: "سبب موجز لعدم الحاجة إلى إجراء أو تقرير (1–500 حرف).",
|
|
77
|
+
emailNotSent: "لم تُرسل رسالة البريد الإلكتروني عبر مزوّد تسليم.",
|
|
78
|
+
noWork: "انتهت الأتمتة دون إجراء مؤكد. اضبط وجهة تسليم للتقارير المخصصة للقراءة فقط.",
|
|
79
|
+
emptyDelivery: "لم تنتج الأتمتة أي رسالة لوجهة التسليم المحددة.",
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
export function automationOutcomeMessagesForLocale(locale) {
|
|
83
|
+
return AUTOMATION_OUTCOME_MESSAGES[isLocaleCode(locale) ? locale : DEFAULT_LOCALE];
|
|
84
|
+
}
|
|
85
|
+
export async function automationOutcomeMessagesForUser(userEmail) {
|
|
86
|
+
const preference = normalizeLocalizationPreference(userEmail
|
|
87
|
+
? await getUserSetting(userEmail, LOCALIZATION_SETTING_KEY)
|
|
88
|
+
: undefined);
|
|
89
|
+
return automationOutcomeMessagesForLocale(preference.locale);
|
|
90
|
+
}
|
|
@@ -1018,9 +1018,11 @@ declare const messages: {
|
|
|
1018
1018
|
scheduleUnavailableLocalTitle: string;
|
|
1019
1019
|
scheduleUnavailableDisabled: string;
|
|
1020
1020
|
scheduleUnavailableNoScheduler: string;
|
|
1021
|
+
scheduleUnavailableMissingSecret: string;
|
|
1021
1022
|
scheduleUnavailableLocal: string;
|
|
1022
1023
|
scheduleUnavailableDisabledFix: string;
|
|
1023
1024
|
scheduleUnavailableLocalFix: string;
|
|
1025
|
+
scheduleUnavailableMissingSecretFix: string;
|
|
1024
1026
|
scheduleUnavailableFixLabel: string;
|
|
1025
1027
|
scheduleUnavailableFixLabelOpen: string;
|
|
1026
1028
|
};
|
|
@@ -940,11 +940,13 @@ const messages = {
|
|
|
940
940
|
scheduleUnknownDetail: "The scheduler status check failed, so the next run times below are unconfirmed. Reload to check again.",
|
|
941
941
|
scheduleUnavailableTitle: "Schedules won't run in this deploy",
|
|
942
942
|
scheduleUnavailableLocalTitle: "Schedules don't run in local development",
|
|
943
|
-
scheduleUnavailableDisabled: "This app was built with recurring jobs turned off, so no scheduled automation will fire.
|
|
944
|
-
scheduleUnavailableNoScheduler: "This
|
|
943
|
+
scheduleUnavailableDisabled: "This app was built with recurring jobs turned off, so no scheduled automation will fire. Webhook-triggered automations and Run now still work.",
|
|
944
|
+
scheduleUnavailableNoScheduler: "This deploy has no scheduler, so scheduled automations won't fire, event-triggered automations stay queued, and crashed runs aren't recovered. Webhook-triggered automations and Run now still work.",
|
|
945
|
+
scheduleUnavailableMissingSecret: "This deploy has a scheduler, but {{secret}} isn't set, so every scheduler request is rejected. Scheduled automations won't fire, event-triggered automations stay queued, and crashed runs aren't recovered. Webhook-triggered automations and Run now still work.",
|
|
945
946
|
scheduleUnavailableLocal: "Schedules stay off on a dev machine unless you opt in. Event- and webhook-triggered automations and Run now still work.",
|
|
946
947
|
scheduleUnavailableDisabledFix: "To enable recurring jobs, set AGENT_NATIVE_DISABLE_RECURRING_JOBS=false in the build environment.",
|
|
947
948
|
scheduleUnavailableLocalFix: "Set AGENT_NATIVE_ENABLE_LOCAL_RECURRING_JOBS=true to run schedules on this machine.",
|
|
949
|
+
scheduleUnavailableMissingSecretFix: "Set {{secret}} in this deployment's environment variables, then redeploy.",
|
|
948
950
|
scheduleUnavailableFixLabel: "Show more",
|
|
949
951
|
scheduleUnavailableFixLabelOpen: "Show less",
|
|
950
952
|
},
|
|
@@ -45,6 +45,9 @@ export interface AgentToolCallMetric {
|
|
|
45
45
|
}
|
|
46
46
|
/** App-defined tool names collapse to `other` so they cannot mint series. */
|
|
47
47
|
export declare function recordAgentToolCall(call: AgentToolCallMetric): void;
|
|
48
|
+
export type TraceWriteStage = "spans" | "summary" | "thread_org" | "write";
|
|
49
|
+
/** Counted once per failed stage of a run, never per span. */
|
|
50
|
+
export declare function recordTraceWriteFailure(stage: TraceWriteStage, error: unknown): void;
|
|
48
51
|
/**
|
|
49
52
|
* Export buffered telemetry before a serverless function can freeze. Never
|
|
50
53
|
* delays a request by more than OBSERVABILITY_FLUSH_TIMEOUT_MS; each provider
|
|
@@ -72,6 +72,9 @@ function instruments() {
|
|
|
72
72
|
flushFailures: meter.createCounter("agent_native.telemetry.flush_failures", {
|
|
73
73
|
description: "Telemetry flushes that timed out or failed; their points were dropped.",
|
|
74
74
|
}),
|
|
75
|
+
traceWriteFailures: meter.createCounter("agent_native.observability.trace_write_failures", {
|
|
76
|
+
description: "Runs whose trace spans or summary could not be persisted; their trace is incomplete or missing.",
|
|
77
|
+
}),
|
|
75
78
|
};
|
|
76
79
|
return cachedInstruments;
|
|
77
80
|
}
|
|
@@ -154,6 +157,13 @@ export function recordAgentToolCall(call) {
|
|
|
154
157
|
...(call.errorType ? { "error.type": call.errorType } : {}),
|
|
155
158
|
});
|
|
156
159
|
}
|
|
160
|
+
/** Counted once per failed stage of a run, never per span. */
|
|
161
|
+
export function recordTraceWriteFailure(stage, error) {
|
|
162
|
+
instruments()?.traceWriteFailures.add(1, {
|
|
163
|
+
"agent_native.observability.stage": stage,
|
|
164
|
+
"error.type": flushErrorType(error),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
157
167
|
function flushErrorType(error) {
|
|
158
168
|
return error instanceof Error && error.name ? error.name : "unknown";
|
|
159
169
|
}
|
|
@@ -54,4 +54,26 @@ export const OBSERVABILITY_MIGRATIONS = [
|
|
|
54
54
|
AND summary.org_id IS NOT NULL;
|
|
55
55
|
`,
|
|
56
56
|
},
|
|
57
|
+
{
|
|
58
|
+
version: 3,
|
|
59
|
+
name: "observability-org-scope-legacy-threads",
|
|
60
|
+
// A thread created without a request org kept org_id NULL, so every review
|
|
61
|
+
// join (thread.org_id = trace org) hid it. Adopt the org only when all of
|
|
62
|
+
// the owner's runs on the thread name the same one.
|
|
63
|
+
sql: `
|
|
64
|
+
UPDATE chat_threads AS thread
|
|
65
|
+
SET org_id = trace_org.org_id
|
|
66
|
+
FROM (
|
|
67
|
+
SELECT thread_id, LOWER(user_id) AS owner_key, MIN(org_id) AS org_id
|
|
68
|
+
FROM agent_trace_summaries
|
|
69
|
+
WHERE thread_id IS NOT NULL AND user_id IS NOT NULL
|
|
70
|
+
AND org_id IS NOT NULL
|
|
71
|
+
GROUP BY thread_id, LOWER(user_id)
|
|
72
|
+
HAVING COUNT(DISTINCT org_id) = 1
|
|
73
|
+
) AS trace_org
|
|
74
|
+
WHERE thread.org_id IS NULL
|
|
75
|
+
AND thread.id = trace_org.thread_id
|
|
76
|
+
AND LOWER(thread.owner_email) = trace_org.owner_key;
|
|
77
|
+
`,
|
|
78
|
+
},
|
|
57
79
|
];
|
|
@@ -27,38 +27,43 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
27
27
|
thumbsUpRate: number;
|
|
28
28
|
avgEvalScore: number;
|
|
29
29
|
} | {
|
|
30
|
+
ok?: undefined;
|
|
30
31
|
error: string;
|
|
31
32
|
message: string;
|
|
32
33
|
summary?: undefined;
|
|
33
34
|
spans?: undefined;
|
|
34
|
-
id?: undefined;
|
|
35
|
-
ok?: undefined;
|
|
36
35
|
} | {
|
|
36
|
+
error?: undefined;
|
|
37
|
+
ok?: undefined;
|
|
37
38
|
message?: undefined;
|
|
38
39
|
summary: import("./types.js").TraceSummary;
|
|
39
40
|
spans: import("./types.js").TraceSpan[];
|
|
40
|
-
|
|
41
|
+
} | {
|
|
41
42
|
error?: undefined;
|
|
42
43
|
ok?: undefined;
|
|
43
|
-
} | {
|
|
44
44
|
message?: undefined;
|
|
45
45
|
summary?: undefined;
|
|
46
46
|
spans?: undefined;
|
|
47
|
+
traceMissing: true;
|
|
47
48
|
id: string;
|
|
49
|
+
} | {
|
|
48
50
|
error?: undefined;
|
|
49
51
|
ok?: undefined;
|
|
52
|
+
message?: undefined;
|
|
53
|
+
summary?: undefined;
|
|
54
|
+
spans?: undefined;
|
|
55
|
+
traceMissing?: undefined;
|
|
56
|
+
id: string;
|
|
50
57
|
} | {
|
|
58
|
+
ok?: undefined;
|
|
51
59
|
message?: undefined;
|
|
52
60
|
summary?: undefined;
|
|
53
61
|
spans?: undefined;
|
|
54
|
-
id?: undefined;
|
|
55
62
|
error: any;
|
|
56
|
-
ok?: undefined;
|
|
57
63
|
} | {
|
|
64
|
+
error?: undefined;
|
|
58
65
|
message?: undefined;
|
|
59
66
|
summary?: undefined;
|
|
60
67
|
spans?: undefined;
|
|
61
|
-
id?: undefined;
|
|
62
|
-
error?: undefined;
|
|
63
68
|
ok: boolean;
|
|
64
69
|
}>>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { defineEventHandler, getHeader, getMethod, getQuery, setResponseHeader, setResponseStatus, } from "h3";
|
|
2
2
|
import { isActionContractError } from "../action.js";
|
|
3
|
+
import { resolveThreadAccess } from "../chat-threads/store.js";
|
|
3
4
|
import { getOrgContext } from "../org/context.js";
|
|
5
|
+
import { isOrgMember } from "../org/membership.js";
|
|
4
6
|
import { getSession } from "../server/auth.js";
|
|
5
7
|
import { readBody } from "../server/h3-helpers.js";
|
|
6
8
|
import { getRequestContext } from "../server/request-context.js";
|
|
@@ -15,6 +17,16 @@ const FEEDBACK_TYPES = [
|
|
|
15
17
|
"category",
|
|
16
18
|
"text",
|
|
17
19
|
];
|
|
20
|
+
const MAX_FEEDBACK_VALUE_CHARS = 20_000;
|
|
21
|
+
const MAX_ID_CHARS = 200;
|
|
22
|
+
// An id past the bound is recorded truncated but never looked up: no real id is
|
|
23
|
+
// that long, and its prefix can name a different row.
|
|
24
|
+
function idClaim(value) {
|
|
25
|
+
if (!value)
|
|
26
|
+
return null;
|
|
27
|
+
const id = String(value);
|
|
28
|
+
return { id: id.slice(0, MAX_ID_CHARS), overlong: id.length > MAX_ID_CHARS };
|
|
29
|
+
}
|
|
18
30
|
function isFeedbackType(value) {
|
|
19
31
|
return (typeof value === "string" &&
|
|
20
32
|
FEEDBACK_TYPES.includes(value));
|
|
@@ -174,33 +186,90 @@ export function createObservabilityHandler() {
|
|
|
174
186
|
return { error: "feedbackType is required" };
|
|
175
187
|
}
|
|
176
188
|
const rawValue = body.value;
|
|
177
|
-
|
|
189
|
+
let value = rawValue == null
|
|
178
190
|
? ""
|
|
179
191
|
: typeof rawValue === "object"
|
|
180
192
|
? JSON.stringify(rawValue)
|
|
181
193
|
: String(rawValue);
|
|
194
|
+
if (value.length > MAX_FEEDBACK_VALUE_CHARS) {
|
|
195
|
+
setResponseStatus(event, 413);
|
|
196
|
+
return { error: "Feedback value is too large" };
|
|
197
|
+
}
|
|
182
198
|
const id = nanoid();
|
|
183
199
|
const idempotencyKey = feedbackType === "text"
|
|
184
200
|
? getHeader(event, "idempotency-key")?.trim() || null
|
|
185
201
|
: null;
|
|
186
202
|
const org = await getOrgContext(event);
|
|
187
|
-
const
|
|
188
|
-
|
|
203
|
+
const runClaim = idClaim(body.runId);
|
|
204
|
+
const threadClaim = idClaim(body.threadId);
|
|
205
|
+
let runId = runClaim?.id ?? null;
|
|
206
|
+
let threadId = threadClaim?.id ?? null;
|
|
189
207
|
let model;
|
|
190
208
|
let orgId = org.orgId;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
209
|
+
let unverifiedRunId;
|
|
210
|
+
let unverifiedThreadId;
|
|
211
|
+
// A thread id is the caller's claim until one of their own runs vouches
|
|
212
|
+
// for it or they are shown to have access to the thread.
|
|
213
|
+
let threadVouched = false;
|
|
214
|
+
if (runId && runClaim?.overlong) {
|
|
215
|
+
unverifiedRunId = runId;
|
|
216
|
+
runId = null;
|
|
217
|
+
}
|
|
218
|
+
else if (runId) {
|
|
219
|
+
// Ownership is the user, not the org: a run recorded with no org, or
|
|
220
|
+
// under another of the caller's orgs, is still the caller's own.
|
|
221
|
+
const summary = await getTraceSummary(runId, { userId: owner });
|
|
222
|
+
// Trace writes are fire-and-forget, so a vote can arrive for a run
|
|
223
|
+
// that was never persisted. That is a missing trace, not a run that
|
|
224
|
+
// belongs to someone else: the latter still answers 404.
|
|
225
|
+
const traceMissing = !summary && !(await getTraceSummary(runId));
|
|
226
|
+
if (traceMissing) {
|
|
227
|
+
unverifiedRunId = runId;
|
|
228
|
+
runId = null;
|
|
229
|
+
}
|
|
230
|
+
else if (!summary ||
|
|
231
|
+
(threadId && (threadClaim?.overlong || threadId !== summary.threadId))) {
|
|
197
232
|
setResponseStatus(event, 404);
|
|
198
233
|
return { error: "Trace not found" };
|
|
199
234
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
235
|
+
else {
|
|
236
|
+
threadId = summary.threadId;
|
|
237
|
+
threadVouched = true;
|
|
238
|
+
model = summary.model || undefined;
|
|
239
|
+
// The run is the caller's, but it may be recorded under an org they
|
|
240
|
+
// have since left; that org's review data is not theirs to write to.
|
|
241
|
+
if (summary.orgId && summary.orgId !== org.orgId) {
|
|
242
|
+
orgId = (await isOrgMember(summary.orgId, owner))
|
|
243
|
+
? summary.orgId
|
|
244
|
+
: null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (threadId &&
|
|
249
|
+
!threadVouched &&
|
|
250
|
+
(threadClaim?.overlong ||
|
|
251
|
+
!(await resolveThreadAccess(owner, threadId, "viewer", {
|
|
252
|
+
orgId: org.orgId ?? undefined,
|
|
253
|
+
})))) {
|
|
254
|
+
// The thread may live in another app (a workspace chat rail posts
|
|
255
|
+
// votes to the host for a remote app's thread), so an unverifiable id
|
|
256
|
+
// is not an error. A missing and an inaccessible thread must answer
|
|
257
|
+
// alike, or the route reveals which thread ids exist.
|
|
258
|
+
unverifiedThreadId = threadId;
|
|
259
|
+
threadId = null;
|
|
260
|
+
}
|
|
261
|
+
const traceMissing = !!(unverifiedRunId || unverifiedThreadId);
|
|
262
|
+
if (traceMissing && rawValue && typeof rawValue === "object") {
|
|
263
|
+
value = JSON.stringify({
|
|
264
|
+
...rawValue,
|
|
265
|
+
traceMissing: true,
|
|
266
|
+
...(unverifiedRunId ? { unverifiedRunId } : {}),
|
|
267
|
+
...(unverifiedThreadId ? { unverifiedThreadId } : {}),
|
|
268
|
+
});
|
|
203
269
|
}
|
|
270
|
+
const traceMissingResult = traceMissing
|
|
271
|
+
? { traceMissing: true }
|
|
272
|
+
: {};
|
|
204
273
|
const inserted = await insertFeedback({
|
|
205
274
|
id,
|
|
206
275
|
runId,
|
|
@@ -215,7 +284,7 @@ export function createObservabilityHandler() {
|
|
|
215
284
|
createdAt: Date.now(),
|
|
216
285
|
});
|
|
217
286
|
if (!inserted)
|
|
218
|
-
return { id };
|
|
287
|
+
return { id, ...traceMissingResult };
|
|
219
288
|
{
|
|
220
289
|
const isThumb = feedbackType === "thumbs_up" || feedbackType === "thumbs_down";
|
|
221
290
|
track("$ai_feedback", {
|
|
@@ -230,6 +299,11 @@ export function createObservabilityHandler() {
|
|
|
230
299
|
run_id: runId,
|
|
231
300
|
thread_id: threadId,
|
|
232
301
|
model,
|
|
302
|
+
...(traceMissing ? { trace_missing: true } : {}),
|
|
303
|
+
...(unverifiedRunId ? { unverified_run_id: unverifiedRunId } : {}),
|
|
304
|
+
...(unverifiedThreadId
|
|
305
|
+
? { unverified_thread_id: unverifiedThreadId }
|
|
306
|
+
: {}),
|
|
233
307
|
$ai_trace_id: runId ?? undefined,
|
|
234
308
|
$ai_session_id: threadId ?? undefined,
|
|
235
309
|
$ai_model: model,
|
|
@@ -255,7 +329,7 @@ export function createObservabilityHandler() {
|
|
|
255
329
|
}).catch(() => { }))
|
|
256
330
|
.catch(() => { });
|
|
257
331
|
}
|
|
258
|
-
return { id };
|
|
332
|
+
return { id, ...traceMissingResult };
|
|
259
333
|
}
|
|
260
334
|
if (method === "GET" && parts.length === 1 && parts[0] === "feedback") {
|
|
261
335
|
setResponseHeader(event, "Cache-Control", "private, no-store");
|
|
@@ -15,8 +15,13 @@ export declare function shouldInferSentimentForTurn(args: {
|
|
|
15
15
|
hasUserText: boolean;
|
|
16
16
|
}): boolean;
|
|
17
17
|
export declare function parseInferredSentiment(output: string): InferredSentiment | null;
|
|
18
|
+
export declare const INFERRED_SENTIMENT_FAILURE_REASONS: readonly ["engine_unavailable", "timeout", "parse_failed", "empty"];
|
|
19
|
+
export type InferredSentimentFailureReason = (typeof INFERRED_SENTIMENT_FAILURE_REASONS)[number];
|
|
18
20
|
export declare function inferAndTrackSentiment(args: {
|
|
21
|
+
/** Explicit engine override; wins over `runEngine`. */
|
|
19
22
|
engine?: AgentEngine;
|
|
23
|
+
/** The engine that served the run being classified. */
|
|
24
|
+
runEngine?: AgentEngine;
|
|
20
25
|
classifierModel: string;
|
|
21
26
|
precedingResponseModel: string;
|
|
22
27
|
text: string;
|
|
@@ -99,19 +99,35 @@ function truncateInput(text) {
|
|
|
99
99
|
.slice(0, INFERRED_SENTIMENT_MAX_CHARS)
|
|
100
100
|
.join("");
|
|
101
101
|
}
|
|
102
|
+
export const INFERRED_SENTIMENT_FAILURE_REASONS = [
|
|
103
|
+
"engine_unavailable",
|
|
104
|
+
"timeout",
|
|
105
|
+
"parse_failed",
|
|
106
|
+
"empty",
|
|
107
|
+
];
|
|
108
|
+
function engineCanRun(engine, model) {
|
|
109
|
+
return (engine.preserveCustomModels === true ||
|
|
110
|
+
engine.supportedModels.length === 0 ||
|
|
111
|
+
engine.supportedModels.includes(model));
|
|
112
|
+
}
|
|
102
113
|
async function classifySentiment(args) {
|
|
103
|
-
if (!args.engine.
|
|
104
|
-
|
|
105
|
-
!args.engine.supportedModels.includes(args.model)) {
|
|
106
|
-
return null;
|
|
114
|
+
if (!engineCanRun(args.engine, args.model)) {
|
|
115
|
+
return { failure: "engine_unavailable" };
|
|
107
116
|
}
|
|
108
117
|
const input = truncateInput(args.text);
|
|
109
118
|
if (!input)
|
|
110
|
-
return
|
|
119
|
+
return { failure: "empty" };
|
|
111
120
|
const controller = new AbortController();
|
|
112
|
-
|
|
121
|
+
let timedOut = false;
|
|
122
|
+
const timeout = setTimeout(() => {
|
|
123
|
+
timedOut = true;
|
|
124
|
+
controller.abort();
|
|
125
|
+
}, INFERRED_SENTIMENT_TIMEOUT_MS);
|
|
113
126
|
let output = "";
|
|
114
127
|
let finalOutput = "";
|
|
128
|
+
const interrupted = () => ({
|
|
129
|
+
failure: timedOut ? "timeout" : "engine_unavailable",
|
|
130
|
+
});
|
|
115
131
|
try {
|
|
116
132
|
for await (const event of args.engine.stream({
|
|
117
133
|
model: args.model,
|
|
@@ -133,51 +149,85 @@ async function classifySentiment(args) {
|
|
|
133
149
|
.join("");
|
|
134
150
|
}
|
|
135
151
|
if (typedEvent.type === "stop" && typedEvent.reason === "error") {
|
|
136
|
-
return
|
|
152
|
+
return interrupted();
|
|
137
153
|
}
|
|
138
154
|
}
|
|
139
|
-
return parseInferredSentiment(finalOutput || output);
|
|
140
155
|
}
|
|
141
156
|
catch {
|
|
142
|
-
return
|
|
157
|
+
return interrupted();
|
|
143
158
|
}
|
|
144
159
|
finally {
|
|
145
160
|
clearTimeout(timeout);
|
|
146
161
|
}
|
|
162
|
+
// An aborted stream can end quietly; its partial text is not a verdict.
|
|
163
|
+
if (timedOut)
|
|
164
|
+
return { failure: "timeout" };
|
|
165
|
+
const verdict = (finalOutput || output).trim();
|
|
166
|
+
if (!verdict)
|
|
167
|
+
return { failure: "empty" };
|
|
168
|
+
const sentiment = parseInferredSentiment(verdict);
|
|
169
|
+
return sentiment ? { sentiment } : { failure: "parse_failed" };
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The run's own engine when it can serve the classifier model, so a Builder run
|
|
173
|
+
* classifies with the credentials it already holds. Otherwise the hosted
|
|
174
|
+
* Builder engine; `null` when that cannot even be constructed.
|
|
175
|
+
*/
|
|
176
|
+
async function resolveClassifierEngine(args) {
|
|
177
|
+
if (args.engine)
|
|
178
|
+
return args.engine;
|
|
179
|
+
if (args.runEngine && engineCanRun(args.runEngine, args.classifierModel)) {
|
|
180
|
+
return args.runEngine;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
return (await import("../agent/engine/builder-engine.js")).createBuilderEngine();
|
|
184
|
+
// coercion-ok: the caller reports a null engine as `engine_unavailable`.
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
147
189
|
}
|
|
148
190
|
export async function inferAndTrackSentiment(args) {
|
|
149
191
|
try {
|
|
150
192
|
if (!shouldSampleInferredSentiment(args.classificationTriggerRunId, args.sampleRate)) {
|
|
151
193
|
return;
|
|
152
194
|
}
|
|
153
|
-
const engine = args
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return;
|
|
195
|
+
const engine = await resolveClassifierEngine(args);
|
|
196
|
+
const result = engine
|
|
197
|
+
? await classifySentiment({
|
|
198
|
+
engine,
|
|
199
|
+
model: args.classifierModel,
|
|
200
|
+
text: args.text,
|
|
201
|
+
})
|
|
202
|
+
: { failure: "engine_unavailable" };
|
|
162
203
|
const { track } = await import("../tracking/registry.js");
|
|
163
|
-
|
|
204
|
+
const shared = {
|
|
164
205
|
...trackingIdentityProperties(),
|
|
165
206
|
source: "agent_observability",
|
|
166
207
|
method: "llm",
|
|
167
|
-
sentiment,
|
|
168
208
|
model: args.precedingResponseModel,
|
|
169
209
|
classifier_model: args.classifierModel,
|
|
170
|
-
classifier_engine: engine
|
|
171
|
-
attribution: "user_reaction_to_preceding_model",
|
|
210
|
+
classifier_engine: engine?.name,
|
|
172
211
|
run_id: args.precedingRunId,
|
|
173
212
|
classification_trigger_run_id: args.classificationTriggerRunId,
|
|
174
213
|
thread_id: args.threadId,
|
|
175
214
|
$ai_model: args.precedingResponseModel,
|
|
176
215
|
$ai_trace_id: args.precedingRunId,
|
|
177
216
|
$ai_session_id: args.threadId ?? undefined,
|
|
178
|
-
}
|
|
217
|
+
};
|
|
218
|
+
// A classifier that fails must show up as a count, never as a quiet drop
|
|
219
|
+
// in `$ai_sentiment`; the reason is coarse and carries no message content.
|
|
220
|
+
track("failure" in result ? "$ai_sentiment_failed" : "$ai_sentiment", "failure" in result
|
|
221
|
+
? { ...shared, reason: result.failure }
|
|
222
|
+
: {
|
|
223
|
+
...shared,
|
|
224
|
+
sentiment: result.sentiment,
|
|
225
|
+
attribution: "user_reaction_to_preceding_model",
|
|
226
|
+
}, { userId: args.userId ?? undefined });
|
|
179
227
|
}
|
|
180
|
-
catch {
|
|
181
|
-
//
|
|
228
|
+
catch (error) {
|
|
229
|
+
// Optional inference must never affect chat, but an unexpected failure
|
|
230
|
+
// outside the classifier must not vanish either.
|
|
231
|
+
console.warn("[agent-native] observability: sentiment inference failed", error instanceof Error ? error.message : String(error));
|
|
182
232
|
}
|
|
183
233
|
}
|
|
@@ -48,6 +48,34 @@ export declare function getTraceSummary(runId: string, opts?: {
|
|
|
48
48
|
userId?: string;
|
|
49
49
|
orgId?: string;
|
|
50
50
|
}): Promise<TraceSummary | null>;
|
|
51
|
+
/**
|
|
52
|
+
* A thread created on a path with no request org keeps `org_id` NULL, and every
|
|
53
|
+
* review read requires `thread.org_id` to equal the trace's org, so its runs
|
|
54
|
+
* never reach Human Review. The run that executed under an org is the proof of
|
|
55
|
+
* which org the thread belongs to: adopt it, for the thread's own owner only
|
|
56
|
+
* and only while `org_id` is NULL.
|
|
57
|
+
*
|
|
58
|
+
* A private thread's `org_id` grants nothing but that review access (owner
|
|
59
|
+
* access ignores it, shares do not read it, and `visibility = 'org'` is the only
|
|
60
|
+
* way it scopes sharing), while `thread_data` holds every run's conversation. So
|
|
61
|
+
* a private thread whose owner's runs span more than one org belongs to none of
|
|
62
|
+
* them: the second statement unassigns it. Org-visible threads keep their
|
|
63
|
+
* `org_id`, which is an explicit sharing scope.
|
|
64
|
+
*
|
|
65
|
+
* Both statements run after this run's own summary is committed
|
|
66
|
+
* (`upsertTraceSummary` is awaited first), so of two concurrent runs under
|
|
67
|
+
* different orgs the one whose statements run later always sees the other's
|
|
68
|
+
* committed org. If this run's adopt raced ahead of the other run's summary, the
|
|
69
|
+
* other run's unassign undoes it, or, when that ran before this adopt
|
|
70
|
+
* committed, this run's own unassign does, since it starts after the adopt.
|
|
71
|
+
* No lock is needed.
|
|
72
|
+
*
|
|
73
|
+
* The multi-org signal is derived from `agent_trace_summaries`, so it is only
|
|
74
|
+
* as durable as those rows: retention purges them, and a run that never wrote a
|
|
75
|
+
* summary is invisible to it. A thread unassigned for spanning orgs can
|
|
76
|
+
* therefore be re-adopted once the other org's summaries age out.
|
|
77
|
+
*/
|
|
78
|
+
export declare function adoptTraceOrgForThread(summary: Pick<TraceSummary, "threadId" | "userId" | "orgId">): Promise<void>;
|
|
51
79
|
export declare function getOrgScopedThreadData(orgId: string, ownerEmail: string, threadIds: readonly string[]): Promise<Map<string, string | null>>;
|
|
52
80
|
export declare function getOrgScopedThreadTitles(orgId: string, ownerEmail: string, threadIds: readonly string[]): Promise<Map<string, string>>;
|
|
53
81
|
export declare function getOrgScopedReviewThreads(requested: readonly (ObservabilityReviewThreadScope & {
|