@teaflask/assistant 0.9.0 → 0.10.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.
@@ -86,9 +86,10 @@ export interface TeaflaskAssistantProviderBaseProps {
86
86
  */
87
87
  onError?: (error: Error) => void;
88
88
  /**
89
- * Called with product analytics events (approval submits and stream
90
- * failures) that you can forward to your own analytics. If it throws, the
91
- * error goes to `onError`.
89
+ * Called with product analytics events (approval submits, stream failures
90
+ * and failed replies) that you can forward to your own analytics. If it
91
+ * throws, the error goes to `onError`. Without it, each failed reply writes
92
+ * one console warning with its reason code instead.
92
93
  */
93
94
  onTelemetry?: (event: AssistantTelemetryEvent) => void;
94
95
  /**
@@ -1,3 +1,5 @@
1
+ export type { FailureReasonCode } from "../generated/models/index.js";
2
+ import type { FailureReasonCode } from "../generated/models/index.js";
1
3
  /**
2
4
  * Why clicking approve or deny sent nothing. "no_active_thread" means no
3
5
  * conversation was open; "card_missing" means the request was no longer shown;
@@ -64,12 +66,31 @@ export interface StreamFailedTelemetryEvent {
64
66
  last_committed_sse_id: string | null;
65
67
  };
66
68
  }
69
+ /**
70
+ * Reported to onTelemetry when a reply fails while the user is watching. The
71
+ * user sees one general sentence, so this event's reason_code is how your page
72
+ * learns why; log it and never show it. Failures replayed when a conversation
73
+ * reopens are not reported again.
74
+ */
75
+ export interface TurnFailedTelemetryEvent {
76
+ /** Always "turn_failed". */
77
+ name: "turn_failed";
78
+ /**
79
+ * The conversation's thread_id, the failed reply's turn_id, and its
80
+ * reason_code, the same code the stream's turn_failed event carries as
81
+ * reasonCode.
82
+ */
83
+ properties: {
84
+ thread_id: string;
85
+ turn_id: string;
86
+ reason_code: FailureReasonCode;
87
+ };
88
+ }
67
89
  /**
68
90
  * Any event passed to the provider's onTelemetry callback, for forwarding to
69
91
  * your own analytics. None carry message text, tool arguments or prompts.
70
92
  */
71
- export type AssistantTelemetryEvent = ApprovalSubmitTelemetryEvent | StreamFailedTelemetryEvent;
93
+ export type AssistantTelemetryEvent = ApprovalSubmitTelemetryEvent | StreamFailedTelemetryEvent | TurnFailedTelemetryEvent;
72
94
  export declare function streamFailureClassOf(error: Error, context: {
73
95
  sawBodyBytes: boolean;
74
96
  }): StreamFailureClass;
75
- export {};
@@ -167,6 +167,7 @@ class SessionEntry {
167
167
  this._reportToEveryRegistrant(error);
168
168
  },
169
169
  onTelemetry: (event) => {
170
+ this._warnOfAFailureNoHostLogs(event);
170
171
  for (const registration of [...this.registrations]) {
171
172
  try {
172
173
  registration.registrant.onTelemetry?.(event);
@@ -230,6 +231,15 @@ class SessionEntry {
230
231
  }
231
232
  return merged;
232
233
  }
234
+ _warnOfAFailureNoHostLogs(event) {
235
+ if (event.name !== "turn_failed" || this._someRegistrantLogsTelemetry()) {
236
+ return;
237
+ }
238
+ console.warn("[teaflask] reply failed:", event.properties.reason_code, event.properties.turn_id);
239
+ }
240
+ _someRegistrantLogsTelemetry() {
241
+ return [...this.registrations].some((registration) => registration.registrant.onTelemetry !== null);
242
+ }
233
243
  _reportToEveryRegistrant(error) {
234
244
  for (const registration of [...this.registrations]) {
235
245
  try {
@@ -1,4 +1,5 @@
1
1
  import type { Message } from "@ag-ui/core";
2
+ import type { AssistantTelemetryEvent } from "../contract/telemetry.js";
2
3
  import { ServingReplayStreamAgent } from "../transport/replay-stream-agent.js";
3
4
  import { type StreamResumeStore } from "../transport/stream-resume.js";
4
5
  import type { TokenSession } from "../transport/token-session.js";
@@ -109,6 +110,8 @@ export interface ConnectionEpochDeps {
109
110
  onRunSettled: () => void;
110
111
  onQuietClose: (stillStreaming: () => boolean) => void;
111
112
  onWorkflowRestarted: () => void;
113
+ onTelemetry: (event: AssistantTelemetryEvent) => void;
114
+ settledFailedTurnId: string | null;
112
115
  }
113
116
  export interface ConnectionEpoch {
114
117
  epochId: number;
@@ -15,7 +15,7 @@ import { toolErrorRecorder, wellFormedToolErrorAnchorsOf, } from "./tool-error-a
15
15
  import { toolDecisionRecorder, wellFormedToolDecisionAnchorsOf, } from "./tool-decision-anchors.js";
16
16
  import { toolDenialRecorder, wellFormedToolDenialAnchorsOf, } from "./tool-denial-anchors.js";
17
17
  import { toolRefusalRecorder, wellFormedToolRefusalAnchorsOf, } from "./tool-refusal-anchors.js";
18
- import { turnFailedMarkerRecorder, wellFormedFailAnchorsOf, } from "./turn-failed-anchors.js";
18
+ import { failedTurnRecorder, turnFailedMarkerRecorder, wellFormedFailAnchorsOf, } from "./turn-failed-anchors.js";
19
19
  import { turnUsageRecorder, wellFormedTurnUsageAnchorsOf, } from "./turn-usage-anchors.js";
20
20
  import { messagesRecorder, streamErrorRecorder } from "./messages-cell.js";
21
21
  import { withdrawnReplyRecorder, withoutWithdrawnReplies, } from "./withdrawn-anchors.js";
@@ -110,6 +110,19 @@ function _runMarkerSubscriptionsOf(agent, deps) {
110
110
  deps.publishMarkerAnchors();
111
111
  }
112
112
  })),
113
+ agent.subscribe(failedTurnRecorder((failed) => {
114
+ if (!_isNewsToTheHost(failed, deps)) {
115
+ return;
116
+ }
117
+ deps.onTelemetry({
118
+ name: "turn_failed",
119
+ properties: {
120
+ thread_id: deps.threadId,
121
+ turn_id: failed.turnId,
122
+ reason_code: failed.reasonCode,
123
+ },
124
+ });
125
+ })),
113
126
  agent.subscribe(subagentDeliveryMarkerRecorder((deliveryRunId, messageId, results) => {
114
127
  if (deps.resume.recordSubagentDelivery(deliveryRunId, messageId, results)) {
115
128
  deps.publishMarkerAnchors();
@@ -124,6 +137,10 @@ function _runMarkerSubscriptionsOf(agent, deps) {
124
137
  })),
125
138
  ];
126
139
  }
140
+ function _isNewsToTheHost(failed, deps) {
141
+ return (failed.turnId !== deps.settledFailedTurnId &&
142
+ deps.resume.recordFailedTurnReported(failed.turnId));
143
+ }
127
144
  function _visibleAnchorOf(messageId, agent, deps) {
128
145
  const visible = _visibleMessagesOf(agent, deps);
129
146
  if (visible.some((message) => message.id === messageId)) {
@@ -117,6 +117,7 @@ function _finishAdoption(store, detail) {
117
117
  resume,
118
118
  approvalInbox,
119
119
  coworkerRequestTurns: detail.turns,
120
+ settledFailedTurnId: _settledFailedTurnIdOf(detail.turns),
120
121
  };
121
122
  if (!continuing) {
122
123
  store.questionDrafts.clearAll();
@@ -152,6 +153,10 @@ function adoptCoworkerRequests(store, turns) {
152
153
  publishApprovalCards(store, active.approvalInbox.cards());
153
154
  }
154
155
  }
156
+ function _settledFailedTurnIdOf(turns) {
157
+ const newest = turns.at(-1);
158
+ return newest?.status === "failed" ? newest.id : null;
159
+ }
155
160
  export function adoptRefreshedDetail(store, detail) {
156
161
  const active = store._active;
157
162
  if (active?.thread.id !== detail.thread.id) {
@@ -168,6 +173,7 @@ export function adoptRefreshedDetail(store, detail) {
168
173
  resume: active.resume,
169
174
  approvalInbox: active.approvalInbox,
170
175
  coworkerRequestTurns: active.coworkerRequestTurns,
176
+ settledFailedTurnId: active.settledFailedTurnId,
171
177
  };
172
178
  }
173
179
  _adoptWireModelPick(store, detail.thread, { evenWhenDirty: false });
@@ -217,6 +223,7 @@ export function enterConversationFromSend(store, thread, turn, sentPick) {
217
223
  resume,
218
224
  approvalInbox,
219
225
  coworkerRequestTurns: [],
226
+ settledFailedTurnId: null,
220
227
  };
221
228
  if (sameModelPick(store._modelPick, sentPick)) {
222
229
  _adoptWireModelPick(store, thread, { evenWhenDirty: true });
@@ -17,6 +17,7 @@ export interface ActiveConversation {
17
17
  resume: StreamResumeStore;
18
18
  approvalInbox: ApprovalInbox;
19
19
  coworkerRequestTurns: readonly ServingAssistantTurn[];
20
+ settledFailedTurnId: string | null;
20
21
  }
21
22
  /**
22
23
  * State and controls for writing and sending messages in the open conversation,
@@ -89,6 +89,8 @@ function _mintEpochDeps(store, active, epochId) {
89
89
  onRunSettled: store.handleRunSettled,
90
90
  onQuietClose: store.handleStreamClosedQuietly,
91
91
  onWorkflowRestarted: store.handleWorkflowRestarted,
92
+ onTelemetry: store.deps.onTelemetry,
93
+ settledFailedTurnId: active.settledFailedTurnId,
92
94
  };
93
95
  }
94
96
  export function retireEpoch(store) {
@@ -1,4 +1,5 @@
1
1
  import { receiptOf, wellFormedReceiptAnchorsOf, withReceiptAnchored, type AnchoredReceipts } from "./receipt-anchors.js";
2
+ import { FailureReasonCode } from "../generated/models/index.js";
2
3
  import type { AgentSubscriber } from "@ag-ui/client";
3
4
  /**
4
5
  * The failure sentences for a failed turn, split by whether they are shown
@@ -10,3 +11,9 @@ export declare const turnFailedReceiptOf: typeof receiptOf;
10
11
  export declare const withTurnFailedAnchored: typeof withReceiptAnchored;
11
12
  export declare const wellFormedFailAnchorsOf: typeof wellFormedReceiptAnchorsOf;
12
13
  export declare function turnFailedMarkerRecorder(onReceiptAnchored: (failedRunId: string, messageId: string, position: keyof AnchoredTurnFailReceipts, receipts: string[]) => void): AgentSubscriber;
14
+ export interface FailedTurn {
15
+ turnId: string;
16
+ reasonCode: FailureReasonCode;
17
+ }
18
+ export declare function failedTurnOf(value: unknown): FailedTurn | null;
19
+ export declare function failedTurnRecorder(onFailedLive: (failed: FailedTurn) => void): AgentSubscriber;
@@ -1,5 +1,6 @@
1
1
  import { receiptMarkerRecorder, receiptOf, wellFormedReceiptAnchorsOf, withReceiptAnchored, } from "./receipt-anchors.js";
2
2
  import { TURN_FAILED_EVENT_NAME } from "../contract/events.js";
3
+ import { FailureReasonCode, } from "../generated/models/index.js";
3
4
  export const EMPTY_TURN_FAILED_ANCHORS = new Map();
4
5
  export const turnFailedReceiptOf = receiptOf;
5
6
  export const withTurnFailedAnchored = withReceiptAnchored;
@@ -7,3 +8,44 @@ export const wellFormedFailAnchorsOf = wellFormedReceiptAnchorsOf;
7
8
  export function turnFailedMarkerRecorder(onReceiptAnchored) {
8
9
  return receiptMarkerRecorder(TURN_FAILED_EVENT_NAME, onReceiptAnchored);
9
10
  }
11
+ export function failedTurnOf(value) {
12
+ if (typeof value !== "object" || value === null) {
13
+ return null;
14
+ }
15
+ const payload = value;
16
+ if (typeof payload.turnId !== "string" || payload.turnId === "") {
17
+ return null;
18
+ }
19
+ return {
20
+ turnId: payload.turnId,
21
+ reasonCode: _knownCodeOf(payload.reasonCode),
22
+ };
23
+ }
24
+ function _knownCodeOf(code) {
25
+ const known = Object.values(FailureReasonCode);
26
+ return known.includes(code)
27
+ ? code
28
+ : FailureReasonCode.internal_error;
29
+ }
30
+ export function failedTurnRecorder(onFailedLive) {
31
+ let pending = null;
32
+ return {
33
+ onRunInitialized() {
34
+ pending = null;
35
+ },
36
+ onCustomEvent({ event }) {
37
+ if (event.name === TURN_FAILED_EVENT_NAME) {
38
+ pending = failedTurnOf(event.value);
39
+ }
40
+ },
41
+ onRunFinishedEvent() {
42
+ pending = null;
43
+ },
44
+ onRunErrorEvent() {
45
+ if (pending !== null) {
46
+ onFailedLive(pending);
47
+ }
48
+ pending = null;
49
+ },
50
+ };
51
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Why a reply failed, coarse on purpose since it is visible in the browser.
3
+ * "model_unavailable": the AI model did not respond; "rate_limited": too many
4
+ * requests right now; "credential_declined": an API key or connected ChatGPT
5
+ * plan was refused; "out_of_credit": your Teaflask organization has no funds to
6
+ * pay for replies; "too_long": the message or conversation is too long;
7
+ * "content_refused": the model's content policy refused the message;
8
+ * "configuration_error": the assistant's setup asks for something its model
9
+ * cannot do; "internal_error": anything else. Treat an unknown code as
10
+ * "internal_error".
11
+ */
12
+ export type FailureReasonCode = (typeof FailureReasonCode)[keyof typeof FailureReasonCode];
13
+ /**
14
+ * Why a reply failed, coarse on purpose since it is visible in the browser.
15
+ * "model_unavailable": the AI model did not respond; "rate_limited": too many
16
+ * requests right now; "credential_declined": an API key or connected ChatGPT
17
+ * plan was refused; "out_of_credit": your Teaflask organization has no funds to
18
+ * pay for replies; "too_long": the message or conversation is too long;
19
+ * "content_refused": the model's content policy refused the message;
20
+ * "configuration_error": the assistant's setup asks for something its model
21
+ * cannot do; "internal_error": anything else. Treat an unknown code as
22
+ * "internal_error".
23
+ */
24
+ export declare const FailureReasonCode: {
25
+ readonly model_unavailable: "model_unavailable";
26
+ readonly rate_limited: "rate_limited";
27
+ readonly credential_declined: "credential_declined";
28
+ readonly out_of_credit: "out_of_credit";
29
+ readonly too_long: "too_long";
30
+ readonly content_refused: "content_refused";
31
+ readonly configuration_error: "configuration_error";
32
+ readonly internal_error: "internal_error";
33
+ };
@@ -0,0 +1,10 @@
1
+ export const FailureReasonCode = {
2
+ model_unavailable: "model_unavailable",
3
+ rate_limited: "rate_limited",
4
+ credential_declined: "credential_declined",
5
+ out_of_credit: "out_of_credit",
6
+ too_long: "too_long",
7
+ content_refused: "content_refused",
8
+ configuration_error: "configuration_error",
9
+ internal_error: "internal_error",
10
+ };
@@ -45,6 +45,7 @@ export * from "./errorEnvelope.js";
45
45
  export * from "./evidenceAfterVoidRecordedMarker.js";
46
46
  export * from "./evidenceAfterVoidRecordedMarkerError.js";
47
47
  export * from "./evidenceAfterVoidRecordedMarkerResult.js";
48
+ export * from "./failureReasonCode.js";
48
49
  export * from "./fileSource.js";
49
50
  export * from "./functionCall.js";
50
51
  export * from "./getServingAssistantThreadParams.js";
@@ -45,6 +45,7 @@ export * from "./errorEnvelope.js";
45
45
  export * from "./evidenceAfterVoidRecordedMarker.js";
46
46
  export * from "./evidenceAfterVoidRecordedMarkerError.js";
47
47
  export * from "./evidenceAfterVoidRecordedMarkerResult.js";
48
+ export * from "./failureReasonCode.js";
48
49
  export * from "./fileSource.js";
49
50
  export * from "./functionCall.js";
50
51
  export * from "./getServingAssistantThreadParams.js";
@@ -1,3 +1,6 @@
1
+ import type { FailureReasonCode } from "./failureReasonCode.js";
1
2
  export interface TurnFailedMarker {
2
3
  receipt: string;
4
+ reasonCode?: FailureReasonCode;
5
+ turnId?: string | null;
3
6
  }
@@ -1,7 +1,7 @@
1
1
  export { TeaflaskAssistantProvider, type TeaflaskAssistantAnonymousProps, type TeaflaskAssistantIdentityProps, type TeaflaskAssistantProviderBaseProps, type TeaflaskAssistantProviderProps, } from "./components/teaflask-assistant-provider.js";
2
2
  export type { AssistantTheme, AssistantThemeDarkTokens, AssistantThemeMode, AssistantThemeTokens, } from "./appearance/theme.js";
3
3
  export type { ActionsAdapter, ActionIntent, ActionIntentAction, ActionIntentGraphQL, CookiesActionsAdapter, HeadersActionsAdapter, RequestActionsAdapter, } from "./contract/actions-adapter.js";
4
- export type { ApprovalSubmitTelemetryEvent, AssistantTelemetryEvent, StreamFailedTelemetryEvent, } from "./contract/telemetry.js";
4
+ export type { ApprovalSubmitTelemetryEvent, AssistantTelemetryEvent, StreamFailedTelemetryEvent, TurnFailedTelemetryEvent, } from "./contract/telemetry.js";
5
5
  export type { AssistantSuggestion, AssistantSuggestionInput, AssistantSuggestionKind, } from "./components/conversation-view.js";
6
6
  export type { IconAdapter, JsonSchema, ToolViewAdapter, ToolViewCall, ToolViewInstance, ToolViewProps, ToolViewRegistration, ToolViewRegistry, } from "./core/tool-view.js";
7
7
  export { useAssistantConversation, type AssistantConversation, type OpenConversation, } from "./components/use-assistant-conversation.js";
package/dist/index.d.ts CHANGED
@@ -14,7 +14,7 @@ export type { ServingErrorCode, ServingErrorCodeOnTheWire, } from "./contract/er
14
14
  export type { ServingAssistantThread, TurnStatus } from "./contract/threads.js";
15
15
  export { activeFoldElapsedOf, settledFoldDurationOf, type BlockTiming, type SegmentDuration, } from "./core/segment-timing.js";
16
16
  export type { MemoryUpdated } from "./core/memory-provenance-anchors.js";
17
- export type { ApprovalSubmitDropReason, ApprovalSubmitTelemetryEvent, AssistantTelemetryEvent, StreamFailedTelemetryEvent, StreamFailureClass, } from "./contract/telemetry.js";
17
+ export type { ApprovalSubmitDropReason, ApprovalSubmitTelemetryEvent, AssistantTelemetryEvent, FailureReasonCode, StreamFailedTelemetryEvent, StreamFailureClass, TurnFailedTelemetryEvent, } from "./contract/telemetry.js";
18
18
  export type { ToolCallDisplay, ToolCallState, ToolCallViewModel, } from "./core/tool-call-display.js";
19
19
  export { TOOL_CALL_ICONS, toolCallPresentationOf, type ToolCallIcon, type ToolCallPresentation, } from "./core/tool-call-presentation.js";
20
20
  export { actionToolName, actionToolViews, resolvedToolViewsOf, type IconAdapter, type JsonSchema, type ResolvedToolView, type ResolvedToolViewIcon, type ToolViewAdapter, type ToolViewCall, type ToolViewInstance, type ToolViewProps, type ToolViewRegistration, type ToolViewRegistry, type ToolViewResolution, type ToolViewThemeMode, } from "./core/tool-view.js";
@@ -36,8 +36,10 @@ export declare class StreamResumeStore {
36
36
  private readonly anchoredResumptions;
37
37
  private readonly anchoredFailedRuns;
38
38
  private readonly anchoredDeliveryRuns;
39
+ private readonly reportedFailedTurns;
39
40
  recordResumeAnchor(runId: string | null, messageId: string, markerValue: unknown, markerTimestamp: number | null): boolean;
40
41
  recordTurnFailedReceipts(failedRunId: string, messageId: string, position: keyof AnchoredTurnFailReceipts, receipts: string[]): boolean;
42
+ recordFailedTurnReported(turnId: string): boolean;
41
43
  recordSubagentDelivery(deliveryRunId: string, messageId: string, results: DeliveredResult[]): boolean;
42
44
  recordToolError(toolCallId: string, errorText: string): void;
43
45
  recordToolRefusal(toolCallId: string, sentence: string): void;
@@ -37,6 +37,7 @@ export class StreamResumeStore {
37
37
  anchoredResumptions = new Set();
38
38
  anchoredFailedRuns = new Set();
39
39
  anchoredDeliveryRuns = new Set();
40
+ reportedFailedTurns = new Set();
40
41
  recordResumeAnchor(runId, messageId, markerValue, markerTimestamp) {
41
42
  const attempt = _wireCounterOf(markerValue, "attempt");
42
43
  const carriesRound = typeof markerValue === "object" &&
@@ -60,6 +61,13 @@ export class StreamResumeStore {
60
61
  this.turnFailedAnchors = withTurnFailedAnchored(this.turnFailedAnchors, messageId, position, receipts);
61
62
  return true;
62
63
  }
64
+ recordFailedTurnReported(turnId) {
65
+ if (this.reportedFailedTurns.has(turnId)) {
66
+ return false;
67
+ }
68
+ this.reportedFailedTurns.add(turnId);
69
+ return true;
70
+ }
63
71
  recordSubagentDelivery(deliveryRunId, messageId, results) {
64
72
  if (this.anchoredDeliveryRuns.has(deliveryRunId)) {
65
73
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teaflask/assistant",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "The AI assistant for your product: drop it into your React app or any page with a script tag.",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "author": "Teaflask",