@genesislcap/ai-assistant 15.14.0 → 15.14.2
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/ai-assistant.api.json +37 -1
- package/dist/ai-assistant.d.ts +55 -0
- package/dist/chat-driver.cjs +285 -9
- package/dist/chat-driver.cjs.map +3 -3
- package/dist/chat-driver.mjs +282 -9
- package/dist/chat-driver.mjs.map +3 -3
- package/dist/custom-elements.json +101 -3
- package/dist/dts/chat-driver-node.d.ts +1 -1
- package/dist/dts/chat-driver-node.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +54 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +1 -1
- package/dist/esm/chat-driver-node.js +9 -1
- package/dist/esm/components/chat-driver/chat-driver.js +154 -22
- package/dist/esm/components/chat-driver/chat-driver.test.js +210 -1
- package/dist/esm/state/debug-event-log.js +1 -1
- package/dist/esm/state/debug-event-log.test.js +2 -0
- package/docs/migration-GENC-1506.md +209 -0
- package/package.json +17 -17
- package/src/chat-driver-node.ts +10 -0
- package/src/components/chat-driver/chat-driver.test.ts +294 -0
- package/src/components/chat-driver/chat-driver.ts +215 -9
- package/src/state/debug-event-log.test.ts +2 -0
- package/src/state/debug-event-log.ts +1 -1
|
@@ -25,6 +25,8 @@ import type {
|
|
|
25
25
|
import {
|
|
26
26
|
BudgetExhaustedError,
|
|
27
27
|
DEFAULT_BUDGET_EXHAUSTED_MESSAGE,
|
|
28
|
+
DEFAULT_PROVIDER_REFUSED_MESSAGE,
|
|
29
|
+
ProviderRefusedError,
|
|
28
30
|
isObservableAIProviderRegistry,
|
|
29
31
|
MalformedFunctionCallError,
|
|
30
32
|
ResponseTruncatedError,
|
|
@@ -80,6 +82,10 @@ import type { AiDriver, AllAgentSummary } from '../ai-driver/ai-driver';
|
|
|
80
82
|
* Derived from the type rather than restated so the two cannot drift.
|
|
81
83
|
*/
|
|
82
84
|
type BudgetDetail = NonNullable<Extract<ChatDriverResult, { reason: 'done' }>['budget']>;
|
|
85
|
+
/** The `providerRefused` payload on a `'provider-refused'` {@link ChatDriverResult} (GENC-1506). */
|
|
86
|
+
type ProviderRefusedDetail = NonNullable<
|
|
87
|
+
Extract<ChatDriverResult, { reason: 'done' }>['providerRefused']
|
|
88
|
+
>;
|
|
83
89
|
|
|
84
90
|
/**
|
|
85
91
|
* Lift the reportable facts off a {@link BudgetExhaustedError}, or `undefined`
|
|
@@ -100,6 +106,21 @@ type BudgetDetail = NonNullable<Extract<ChatDriverResult, { reason: 'done' }>['b
|
|
|
100
106
|
* `formatBlockedReason`, which returns `undefined` for a figure-less budget so a
|
|
101
107
|
* host-set explanation survives the latch.
|
|
102
108
|
*/
|
|
109
|
+
/**
|
|
110
|
+
* Lift the reportable facts off a {@link ProviderRefusedError} (GENC-1506).
|
|
111
|
+
*
|
|
112
|
+
* Unlike `budgetDetailOf` this is total — it always returns a detail. The budget version can answer
|
|
113
|
+
* `undefined` because its payload is figures the proxy may not have sent; here every field either
|
|
114
|
+
* comes from the transport that was refused (`vendorLabel`, `kind`) or is plainly optional, so there
|
|
115
|
+
* is no "nothing worth reporting" case to model.
|
|
116
|
+
*/
|
|
117
|
+
const providerRefusedDetailOf = (e: ProviderRefusedError): ProviderRefusedDetail => ({
|
|
118
|
+
vendorLabel: e.vendorLabel,
|
|
119
|
+
kind: e.kind,
|
|
120
|
+
...(e.upstreamStatus != null ? { upstreamStatus: e.upstreamStatus } : {}),
|
|
121
|
+
...(e.upstreamType ? { upstreamType: e.upstreamType } : {}),
|
|
122
|
+
});
|
|
123
|
+
|
|
103
124
|
const budgetDetailOf = (e: BudgetExhaustedError): BudgetDetail | undefined => {
|
|
104
125
|
// The typed vendor is derived from the LABEL, not from `lastResolvedProvider`:
|
|
105
126
|
// the label comes from the transport that was actually refused, whereas the
|
|
@@ -352,6 +373,20 @@ export interface ChatDriverConfig {
|
|
|
352
373
|
* reversible change.
|
|
353
374
|
*/
|
|
354
375
|
budgetExhaustedMessage?: string;
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Overrides the transcript sentence shown when the upstream PROVIDER refuses the account
|
|
379
|
+
* (GENC-1506) — see `DEFAULT_PROVIDER_REFUSED_MESSAGE`.
|
|
380
|
+
*
|
|
381
|
+
* Separate from `budgetExhaustedMessage` because the two conditions are separate: ours is a spend
|
|
382
|
+
* cap a host administrator can raise, this is the vendor declining to serve us at all. A host that
|
|
383
|
+
* white-labels one will usually want to white-label both, but conflating them into one field would
|
|
384
|
+
* force identical copy on two situations with different remedies.
|
|
385
|
+
*
|
|
386
|
+
* Rarely needed: unlike the budget default, the shipped sentence names no vendor and no Genesis, so
|
|
387
|
+
* it is already safe for a white-labelled deployment.
|
|
388
|
+
*/
|
|
389
|
+
providerRefusedMessage?: string;
|
|
355
390
|
}
|
|
356
391
|
|
|
357
392
|
/**
|
|
@@ -549,7 +584,21 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
549
584
|
* the child's ATTRIBUTION, not just the fact of a wall — see
|
|
550
585
|
* `budgetWallDetail`.
|
|
551
586
|
*/
|
|
552
|
-
private subAgentFailure:
|
|
587
|
+
private subAgentFailure:
|
|
588
|
+
| {
|
|
589
|
+
reason: SubAgentFailureReason;
|
|
590
|
+
budget?: BudgetDetail;
|
|
591
|
+
/**
|
|
592
|
+
* The refusing vendor and kind, when a child hit the PROVIDER wall (GENC-1506).
|
|
593
|
+
*
|
|
594
|
+
* Carried for the same reason `budget` is: the child is a **separate driver instance**, so
|
|
595
|
+
* nothing it sets on itself is visible to the parent. Without this the parent could report
|
|
596
|
+
* `provider-refused` but not say which vendor or which kind — losing the only signal that
|
|
597
|
+
* distinguishes "top up the account" from "rotate the key", since the user copy is cause-free.
|
|
598
|
+
*/
|
|
599
|
+
providerRefused?: ProviderRefusedDetail;
|
|
600
|
+
}
|
|
601
|
+
| undefined;
|
|
553
602
|
/**
|
|
554
603
|
* Set by `releaseAgent` inside a top-level tool handler — typically a stateful
|
|
555
604
|
* agent's terminal-state handler signalling that its flow is complete and the
|
|
@@ -712,6 +761,27 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
712
761
|
* and the bubble is composed per wall by {@link ChatDriver.budgetExhaustedBubble}.
|
|
713
762
|
*/
|
|
714
763
|
private readonly budgetExhaustedMessageOverride?: string;
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Host override for the provider-refusal sentence, from
|
|
767
|
+
* `ChatDriverConfig.providerRefusedMessage`. `undefined` means no override, so the shipped default
|
|
768
|
+
* is used verbatim.
|
|
769
|
+
*/
|
|
770
|
+
private readonly providerRefusedMessageOverride?: string;
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* A provider refusal was observed this turn, so the tool loop must not call the model again
|
|
774
|
+
* (GENC-1506).
|
|
775
|
+
*
|
|
776
|
+
* Exists for the SUB-AGENT path only, exactly like `budgetExhaustedThisTurn`: this driver's own
|
|
777
|
+
* refusal returns straight out of the catch, whereas a child's refusal reaches the parent as a tool
|
|
778
|
+
* result, and without this flag the loop would issue another call into the same wall — N batched
|
|
779
|
+
* children costing N doomed calls plus a doomed parent one.
|
|
780
|
+
*/
|
|
781
|
+
private providerRefusedThisTurn = false;
|
|
782
|
+
|
|
783
|
+
/** The refusal detail latched off a sub-agent's failure, for the parent's own result. */
|
|
784
|
+
private providerRefusedDetail?: ProviderRefusedDetail;
|
|
715
785
|
/**
|
|
716
786
|
* Set the moment a budget wall is observed anywhere in this turn — this
|
|
717
787
|
* driver's own 402, or a sub-agent's (which surfaces here only as a
|
|
@@ -761,12 +831,14 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
761
831
|
sessionKey = '',
|
|
762
832
|
activityBus = NOOP_ACTIVITY_BUS,
|
|
763
833
|
budgetExhaustedMessage,
|
|
834
|
+
providerRefusedMessage,
|
|
764
835
|
} = config;
|
|
765
836
|
this.maxToolIterations = maxToolIterations;
|
|
766
837
|
this.condenseBatchCalls = condenseBatchCalls;
|
|
767
838
|
this.sessionKey = sessionKey;
|
|
768
839
|
this.activityBus = activityBus;
|
|
769
840
|
this.budgetExhaustedMessageOverride = budgetExhaustedMessage;
|
|
841
|
+
this.providerRefusedMessageOverride = providerRefusedMessage;
|
|
770
842
|
if (typeof toolHandlers === 'function') {
|
|
771
843
|
this.toolHandlersFactory = toolHandlers;
|
|
772
844
|
this.toolHandlers = {};
|
|
@@ -895,11 +967,20 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
895
967
|
* (rather than set to `undefined`) so a happy-path result stays byte-identical to
|
|
896
968
|
* the historical `{ reason: 'done' }`.
|
|
897
969
|
*/
|
|
898
|
-
private turnDone(
|
|
970
|
+
private turnDone(
|
|
971
|
+
failureReason?: TurnFailureReason,
|
|
972
|
+
budget?: BudgetDetail,
|
|
973
|
+
providerRefused?: ProviderRefusedDetail,
|
|
974
|
+
): ChatDriverResult {
|
|
899
975
|
if (!failureReason) return { reason: 'done' };
|
|
900
|
-
//
|
|
901
|
-
//
|
|
902
|
-
return
|
|
976
|
+
// Both payloads are omitted rather than set to `undefined`, so a failure that carries neither has
|
|
977
|
+
// a shape unchanged for a consumer that structurally compares it.
|
|
978
|
+
return {
|
|
979
|
+
reason: 'done',
|
|
980
|
+
failureReason,
|
|
981
|
+
...(budget ? { budget } : {}),
|
|
982
|
+
...(providerRefused ? { providerRefused } : {}),
|
|
983
|
+
};
|
|
903
984
|
}
|
|
904
985
|
|
|
905
986
|
/**
|
|
@@ -982,6 +1063,44 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
982
1063
|
return this.turnDone('budget-exhausted', budgetDetailOf(e));
|
|
983
1064
|
}
|
|
984
1065
|
|
|
1066
|
+
/**
|
|
1067
|
+
* The sentence shown when the upstream provider refuses (GENC-1506).
|
|
1068
|
+
*
|
|
1069
|
+
* Deliberately far simpler than `budgetExhaustedBubble`: no vendor name, no figures, no
|
|
1070
|
+
* switch-provider advice, and no branching on `kind`. The copy is cause-free by design — a user who
|
|
1071
|
+
* can see their own remaining spend must not be told about a limit, and we must not imply the bill
|
|
1072
|
+
* has gone unpaid — so there is nothing here to compose. A host override wins verbatim.
|
|
1073
|
+
*/
|
|
1074
|
+
private providerRefusedBubble(): string {
|
|
1075
|
+
return this.providerRefusedMessageOverride ?? DEFAULT_PROVIDER_REFUSED_MESSAGE;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Terminal provider-refusal outcome. Mirrors `reportBudgetExhausted` so the two walls behave
|
|
1080
|
+
* identically from the caller's side, while keeping their diagnostics distinct.
|
|
1081
|
+
*
|
|
1082
|
+
* `kind` reaches the debug log and the result but never the transcript: it is what lets an operator
|
|
1083
|
+
* tell "top up the account" from "rotate the key", and with cause-free user copy this is the only
|
|
1084
|
+
* place that distinction survives.
|
|
1085
|
+
*/
|
|
1086
|
+
private reportProviderRefused(e: ProviderRefusedError): ChatDriverResult {
|
|
1087
|
+
const detail = providerRefusedDetailOf(e);
|
|
1088
|
+
this.providerRefusedThisTurn = true;
|
|
1089
|
+
this.providerRefusedDetail = detail;
|
|
1090
|
+
logger.error('ChatDriver: provider refused the request', e);
|
|
1091
|
+
recordTurnError(this.sessionKey, 'provider-refused', {
|
|
1092
|
+
agent: this.activeAgentName,
|
|
1093
|
+
provider: this.lastResolvedProviderName,
|
|
1094
|
+
vendor: vendorTypeOfLabel(e.vendorLabel) ?? this.lastResolvedProvider,
|
|
1095
|
+
kind: e.kind,
|
|
1096
|
+
upstreamStatus: e.upstreamStatus,
|
|
1097
|
+
upstreamType: e.upstreamType,
|
|
1098
|
+
isSubAgent: this.isSubAgent,
|
|
1099
|
+
});
|
|
1100
|
+
this.appendToHistory({ role: 'assistant', content: this.providerRefusedBubble() });
|
|
1101
|
+
return this.turnDone('provider-refused', undefined, detail);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
985
1104
|
/** The typed failure reason on a loop result, or `undefined` for a clean turn / handoff. */
|
|
986
1105
|
private static failureReasonOf(result: ChatDriverResult): TurnFailureReason | undefined {
|
|
987
1106
|
return result.reason === 'done' ? result.failureReason : undefined;
|
|
@@ -1259,7 +1378,13 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1259
1378
|
* `completeSubAgent`, if any. Called by a parent `ChatDriver` after running
|
|
1260
1379
|
* this instance as a sub-agent.
|
|
1261
1380
|
*/
|
|
1262
|
-
getSubAgentFailure():
|
|
1381
|
+
getSubAgentFailure():
|
|
1382
|
+
| {
|
|
1383
|
+
reason: SubAgentFailureReason;
|
|
1384
|
+
budget?: BudgetDetail;
|
|
1385
|
+
providerRefused?: ProviderRefusedDetail;
|
|
1386
|
+
}
|
|
1387
|
+
| undefined {
|
|
1263
1388
|
return this.subAgentFailure;
|
|
1264
1389
|
}
|
|
1265
1390
|
|
|
@@ -1271,11 +1396,19 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1271
1396
|
* under a separate session key, so recording here would orphan the event off
|
|
1272
1397
|
* the user-visible debug-log timeline.)
|
|
1273
1398
|
*/
|
|
1274
|
-
private failSubAgent(
|
|
1399
|
+
private failSubAgent(
|
|
1400
|
+
reason: SubAgentFailureReason,
|
|
1401
|
+
budget?: BudgetDetail,
|
|
1402
|
+
providerRefused?: ProviderRefusedDetail,
|
|
1403
|
+
): void {
|
|
1275
1404
|
if (!this.isSubAgent || this.subAgentFailure) return;
|
|
1276
|
-
//
|
|
1405
|
+
// Each payload is omitted rather than set to `undefined` so a failure carrying neither has a shape
|
|
1277
1406
|
// unchanged for a structural comparison, matching `turnDone`.
|
|
1278
|
-
this.subAgentFailure =
|
|
1407
|
+
this.subAgentFailure = {
|
|
1408
|
+
reason,
|
|
1409
|
+
...(budget ? { budget } : {}),
|
|
1410
|
+
...(providerRefused ? { providerRefused } : {}),
|
|
1411
|
+
};
|
|
1279
1412
|
}
|
|
1280
1413
|
|
|
1281
1414
|
/**
|
|
@@ -1924,6 +2057,8 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1924
2057
|
this.budgetExhaustedThisTurn = false;
|
|
1925
2058
|
this.budgetWallDetail = undefined;
|
|
1926
2059
|
this.budgetWallViaSubAgent = false;
|
|
2060
|
+
this.providerRefusedThisTurn = false;
|
|
2061
|
+
this.providerRefusedDetail = undefined;
|
|
1927
2062
|
this.appendToHistory({ role: 'user', content: userInput, attachments });
|
|
1928
2063
|
this.turnStartedAt = Date.now();
|
|
1929
2064
|
recordMetaEvent(this.sessionKey, 'turn.start', {
|
|
@@ -2321,6 +2456,20 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
2321
2456
|
this.budgetWallViaSubAgent = true;
|
|
2322
2457
|
this.budgetWallDetail ??= failure?.budget;
|
|
2323
2458
|
}
|
|
2459
|
+
// The PROVIDER wall via a child (GENC-1506) — the same shape as the budget wall above, and needed
|
|
2460
|
+
// for the same non-obvious reason: **the child is a separate driver instance**, so the flag it set
|
|
2461
|
+
// on itself is invisible here. Without this branch the child's failure was appended as an ordinary
|
|
2462
|
+
// tool result, the parent's short-circuit never fired, and the parent issued another model call
|
|
2463
|
+
// straight into the same wall. Worse, if that call happened to succeed the turn could finish
|
|
2464
|
+
// WITHOUT `failureReason: 'provider-refused'` at all — reporting a clean turn over a dead account.
|
|
2465
|
+
//
|
|
2466
|
+
// FIRST attribution wins (`??=`), for the reason spelled out on `budgetWallDetail`: batched
|
|
2467
|
+
// delegations can pair an attributable child refusal with an unattributable one, and a plain
|
|
2468
|
+
// assignment would let the later `undefined` erase what the earlier child knew.
|
|
2469
|
+
if (reason === 'provider_refused') {
|
|
2470
|
+
this.providerRefusedThisTurn = true;
|
|
2471
|
+
this.providerRefusedDetail ??= failure?.providerRefused;
|
|
2472
|
+
}
|
|
2324
2473
|
return { outcome: { ok: false, reason }, trace };
|
|
2325
2474
|
}
|
|
2326
2475
|
|
|
@@ -2338,6 +2487,8 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
2338
2487
|
this.budgetExhaustedThisTurn = false;
|
|
2339
2488
|
this.budgetWallDetail = undefined;
|
|
2340
2489
|
this.budgetWallViaSubAgent = false;
|
|
2490
|
+
this.providerRefusedThisTurn = false;
|
|
2491
|
+
this.providerRefusedDetail = undefined;
|
|
2341
2492
|
this.turnStartedAt = Date.now();
|
|
2342
2493
|
recordMetaEvent(this.sessionKey, 'turn.start', {
|
|
2343
2494
|
phase: 'continueFromHistory',
|
|
@@ -2612,6 +2763,30 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
2612
2763
|
// Scoped strictly to the budget reason: every other SubAgentFailureReason
|
|
2613
2764
|
// is something the parent can legitimately recover from, so those still
|
|
2614
2765
|
// let the loop continue.
|
|
2766
|
+
// A provider refusal seen earlier this turn ends it HERE, before another model call. Reachable
|
|
2767
|
+
// only via a sub-agent — this driver's own refusal returns straight out of the catch — and
|
|
2768
|
+
// without it the loop would call the provider again into the same wall (GENC-1506).
|
|
2769
|
+
if (this.providerRefusedThisTurn) {
|
|
2770
|
+
logger.error('ChatDriver: ending the turn — a sub-agent hit the provider wall');
|
|
2771
|
+
recordTurnError(this.sessionKey, 'provider-refused', {
|
|
2772
|
+
agent: this.activeAgentName,
|
|
2773
|
+
provider: this.lastResolvedProviderName,
|
|
2774
|
+
kind: this.providerRefusedDetail?.kind,
|
|
2775
|
+
via: 'sub-agent',
|
|
2776
|
+
isSubAgent: this.isSubAgent,
|
|
2777
|
+
});
|
|
2778
|
+
if (this.isSubAgent) {
|
|
2779
|
+
// The detail is forwarded, not just the reason — this driver may itself be an INTERMEDIATE
|
|
2780
|
+
// sub-agent, and a refusal that started in a grandchild reaches its own parent only through
|
|
2781
|
+
// this call. Dropping it here still stopped the top-level turn (the reason travels), but
|
|
2782
|
+
// arrived with no vendor and no kind, which is the whole diagnostic this payload exists to
|
|
2783
|
+
// carry across hops. The adjacent budget path forwards `budgetWallDetail` for the same reason.
|
|
2784
|
+
this.failSubAgent('provider_refused', undefined, this.providerRefusedDetail);
|
|
2785
|
+
} else {
|
|
2786
|
+
this.appendToHistory({ role: 'assistant', content: this.providerRefusedBubble() });
|
|
2787
|
+
}
|
|
2788
|
+
return this.turnDone('provider-refused', undefined, this.providerRefusedDetail);
|
|
2789
|
+
}
|
|
2615
2790
|
if (this.budgetExhaustedThisTurn) {
|
|
2616
2791
|
logger.error('ChatDriver: ending the turn — a sub-agent hit the AI budget wall');
|
|
2617
2792
|
recordTurnError(this.sessionKey, 'budget-exhausted', {
|
|
@@ -2959,6 +3134,37 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
2959
3134
|
// retry here and no "try again" in the copy; the turn ends and the host
|
|
2960
3135
|
// locks the composer off the `'budget-exhausted'` failure reason (see
|
|
2961
3136
|
// `FoundationAiAssistant.blocked`).
|
|
3137
|
+
// The upstream vendor refused this ACCOUNT (GENC-1506) — its credit is gone, a usage cap is
|
|
3138
|
+
// reached, or the credential is dead. Terminal in the strongest sense available: unlike a
|
|
3139
|
+
// truncation, no smaller request gets past it, and unlike our own budget wall nobody in this
|
|
3140
|
+
// system can raise anything to clear it. So no retry and no "try again" in the copy.
|
|
3141
|
+
//
|
|
3142
|
+
// Placed before the transient-retry step below, which is the whole point: without this the
|
|
3143
|
+
// error is an untyped transport failure, gets re-issued MAX_SETUP_TRANSPORT_RETRIES times
|
|
3144
|
+
// against a wall that cannot move, and then surfaces as "something went wrong on my end" —
|
|
3145
|
+
// wrong twice over, since nothing went wrong on our end and trying again will not help.
|
|
3146
|
+
if (e instanceof ProviderRefusedError) {
|
|
3147
|
+
if (this.isSubAgent) {
|
|
3148
|
+
logger.error('ChatDriver: provider refused the request', e);
|
|
3149
|
+
recordTurnError(this.sessionKey, 'provider-refused', {
|
|
3150
|
+
agent: this.activeAgentName,
|
|
3151
|
+
provider: this.lastResolvedProviderName,
|
|
3152
|
+
vendor: vendorTypeOfLabel(e.vendorLabel) ?? this.lastResolvedProvider,
|
|
3153
|
+
kind: e.kind,
|
|
3154
|
+
upstreamStatus: e.upstreamStatus,
|
|
3155
|
+
upstreamType: e.upstreamType,
|
|
3156
|
+
isSubAgent: true,
|
|
3157
|
+
});
|
|
3158
|
+
// The detail is BUBBLED, not merely flagged. Setting `providerRefusedThisTurn` on `this`
|
|
3159
|
+
// would be pointless here — `this` is the CHILD, and the parent that must stop is a
|
|
3160
|
+
// different driver instance. `invokeSubAgent` reads this payload off the child and sets its
|
|
3161
|
+
// own flag; see the branch there.
|
|
3162
|
+
const detail = providerRefusedDetailOf(e);
|
|
3163
|
+
this.failSubAgent('provider_refused', undefined, detail);
|
|
3164
|
+
return this.turnDone('provider-refused', undefined, detail);
|
|
3165
|
+
}
|
|
3166
|
+
return this.reportProviderRefused(e);
|
|
3167
|
+
}
|
|
2962
3168
|
if (e instanceof BudgetExhaustedError) {
|
|
2963
3169
|
// Flagged as well as returned: a sub-agent's wall reaches the PARENT
|
|
2964
3170
|
// only as a tool result, and the parent must not issue another model
|
|
@@ -109,6 +109,7 @@ const ALL_TURN_FAILURE_REASONS = Object.keys({
|
|
|
109
109
|
'response-truncated': true,
|
|
110
110
|
refusal: true,
|
|
111
111
|
'budget-exhausted': true,
|
|
112
|
+
'provider-refused': true,
|
|
112
113
|
} satisfies Record<TurnFailureReason, true>) as TurnFailureReason[];
|
|
113
114
|
|
|
114
115
|
const ALL_SUB_AGENT_FAILURE_REASONS = Object.keys({
|
|
@@ -120,6 +121,7 @@ const ALL_SUB_AGENT_FAILURE_REASONS = Object.keys({
|
|
|
120
121
|
response_truncated: true,
|
|
121
122
|
refusal: true,
|
|
122
123
|
budget_exhausted: true,
|
|
124
|
+
provider_refused: true,
|
|
123
125
|
} satisfies Record<SubAgentFailureReason, true>) as SubAgentFailureReason[];
|
|
124
126
|
|
|
125
127
|
const README_TEXT = DEBUG_LOG_README.join('\n');
|
|
@@ -328,7 +328,7 @@ export const DEBUG_LOG_README: readonly string[] = [
|
|
|
328
328
|
"kind:'turn'.`agentSnapshot` — the active agent's own view of its internal state, captured at that turn. An agent opts into this by exposing a `getDebugSnapshot()` that returns JSON-serializable per-state info; stateful/flow agents wire it automatically, so you can watch a flow advance turn-by-turn (e.g. current step, cursor, collected fields, pending changes). Absent for agents that don't expose one.",
|
|
329
329
|
"kind:'event' — a meta/lifecycle event. `type` names it (see below); `detail` carries structured data. `detail.placement` is the emitting UI instance: 'bubble' (collapsed), 'panel' (popped-out), or 'standalone'.",
|
|
330
330
|
"Each 'event' also has an `importance`: 'high' (failures/limits — turn.error, tool.failed, subagent.failed, file.read-failed, suggestions.failed, context.threshold-crossed), 'normal' (session flow — connects, turns, retries, handoffs, agent/provider changes, interactions, sub-agent start/complete), or 'low' (skippable UI/bookkeeping noise — panel.toggled, attachment.added, driver.wired/unwired, context.updated, context.condensed). To skim, ignore importance:'low'; to triage a failure, filter to importance:'high' then read the nearby messages and turns. A 'high' turn.error is often preceded by one or more 'normal' turn.retry events for the same reason — read them together to see how many attempts were made before bailing. 'message' and 'turn' entries carry no importance — they are the substance, always read them.",
|
|
331
|
-
'Event types: assistant.connected/disconnected (mount + placement + whether the session was created or restored), assistant.popout/popin (window placement), driver.created/wired/unwired (which driver is live and why it stops/starts responding across a popout), state.changed (idle↔loading), turn.start/turn.end (turn boundary; turn.end carries durationMs), turn.retry (a recoverable in-turn retry — detail.reason plus attempt/maxAttempts; for malformed calls also finishMessage; for empty responses also the provider finishReason + thoughtsTokens + parts breakdown), turn.error (a turn failed or hit a guardrail — detail.reason is one of exception/malformed-function-call/empty-response/unknown-tool-limit/max-iterations/response-truncated/refusal/budget-exhausted, plus reason-specific diagnostics: attempts (for empty-response also finishReason + thoughtsTokens + a parts breakdown, distinguishing a thinking-only STOP from a truly empty turn), finishMessage, for response-truncated the model + maxTokens + outputTokens + tools, unknownTools (split into staleTools — real earlier this activation but retired by the current state or hidden behind an open exclusive fold — and hallucinatedTools — never advertised) + availableTools, iterations + limit, for budget-exhausted the budgetUsd + spentUsd figures reported by the proxy plus the resolved vendor, or name + message for exceptions), tool.failed (a tool threw), tool.unresolved (the model called a tool that could not be dispatched — detail.kind is folded/fold-hidden/stale/unknown, plus tool + agent and, for the counted kinds, the consecutive streak; the recurring lead-up to an unknown-tool-limit turn.error), subagent.started/completed/failed (the lifecycle of a `requestSubAgent` delegation — detail.agent names the sub-agent; these bracket the sub-agent turns that appear as kind:turn entries with an N-M `turnIndex`; subagent.failed also carries detail.reason, one of max_iterations/malformed_tool_call/empty_response/unknown_tool_limit/timeout/response_truncated/refusal/budget_exhausted; budget_exhausted
|
|
331
|
+
'Event types: assistant.connected/disconnected (mount + placement + whether the session was created or restored), assistant.popout/popin (window placement), driver.created/wired/unwired (which driver is live and why it stops/starts responding across a popout), state.changed (idle↔loading), turn.start/turn.end (turn boundary; turn.end carries durationMs), turn.retry (a recoverable in-turn retry — detail.reason plus attempt/maxAttempts; for malformed calls also finishMessage; for empty responses also the provider finishReason + thoughtsTokens + parts breakdown), turn.error (a turn failed or hit a guardrail — detail.reason is one of exception/malformed-function-call/empty-response/unknown-tool-limit/max-iterations/response-truncated/refusal/budget-exhausted/provider-refused, plus reason-specific diagnostics: attempts (for empty-response also finishReason + thoughtsTokens + a parts breakdown, distinguishing a thinking-only STOP from a truly empty turn), finishMessage, for response-truncated the model + maxTokens + outputTokens + tools, unknownTools (split into staleTools — real earlier this activation but retired by the current state or hidden behind an open exclusive fold — and hallucinatedTools — never advertised) + availableTools, iterations + limit, for budget-exhausted the budgetUsd + spentUsd figures reported by the proxy plus the resolved vendor, for provider-refused the kind (spend|auth) plus the upstream status and error type — the vendor refusing the ACCOUNT rather than us refusing to spend, so the remedy is a top-up or a key rotation rather than a raised cap, and the kind is the only place that distinction survives because the user-facing copy is deliberately cause-free, or name + message for exceptions), tool.failed (a tool threw), tool.unresolved (the model called a tool that could not be dispatched — detail.kind is folded/fold-hidden/stale/unknown, plus tool + agent and, for the counted kinds, the consecutive streak; the recurring lead-up to an unknown-tool-limit turn.error), subagent.started/completed/failed (the lifecycle of a `requestSubAgent` delegation — detail.agent names the sub-agent; these bracket the sub-agent turns that appear as kind:turn entries with an N-M `turnIndex`; subagent.failed also carries detail.reason, one of max_iterations/malformed_tool_call/empty_response/unknown_tool_limit/timeout/response_truncated/refusal/budget_exhausted/provider_refused; budget_exhausted and provider_refused are both terminal for the PARENT turn too — the parent stops rather than calling the model again into the same wall), agent.handoff (routing; from=null is the initial activation), agent.pinned/unpinned (forced routing), provider.selected (the resolved provider for the upcoming turns — detail.provider is the registry SLOT/tier name, detail.model the concrete model behind it and detail.vendor its vendor; emitted only when the slot CHANGES, so read the per-turn `model` for the model of any given call rather than assuming the nearest event still applies), interaction.requested/resolved (blocking user widgets — explain quiet gaps; note that when a sub-agent opens a widget, detail.agent — and the agentName on the interaction message — is the HOST agent that owns the widget, NOT the sub-agent that asked, because widgets render and resolve on the host driver), context.updated/threshold-crossed (token + cost), context.condensed (a stale tool payload was collapsed out of the model-bound history by a `condenseWhen` declaration on the tool — detail.tool + toolCallId, target args|response, trigger (superseded:<key> or age:<n>), stubLen, and an estimated tokensSaved; stored history and this log keep the FULL payload, so the model-visible slice at any point is the full history minus the condensations recorded up to then), panel.toggled, attachment.added, file.read-failed, suggestions.failed.',
|
|
332
332
|
'Sub-agent meta events: a sub-agent\'s own turn.retry/turn.error/tool.failed/tool.unresolved events are merged into this same timeline, tagged with `detail.subAgent` — a `"<parent> › <sub-agent>"` breadcrumb that composes when nested (e.g. `"UI Builder › Planner › Grounding"`) — and interleaved by their original timestamps within the subagent.started→completed/failed bracket. These are the per-attempt/per-failure signals that do NOT appear among the sub-agent\'s (hoisted) messages: a malformed/empty attempt that gets retried produces no message, and the stale-vs-hallucinated split and streak counts live only on the event. A sub-agent\'s high-volume, message-derivable events (turn.start/turn.end, provider.selected, context.updated) are intentionally NOT merged — read its hoisted messages for model/tokens/cost and turn-by-turn activity, and the bracketing subagent.* events for the run\'s span.',
|
|
333
333
|
"`meta` holds context captured at export time: agentSummary (full agent configs), context (active model, contextTokens/contextLimit/contextUsagePercent for the last call, and the session totals — sessionCostUsd, sessionTokensConsumed, and the four-bucket sessionUsage lifted to the top of this log), activeDebugSnapshot (the active agent's `getDebugSnapshot()` taken fresh at export — reflects state NOW, which may have advanced beyond the last turn's agentSnapshot), debug (optional host-supplied debug state), host, and the export timestamp.",
|
|
334
334
|
'Note the two different scopes in `meta.context`: `contextTokens` is the prompt size of the LAST call (against `contextLimit`, the model context window), while `sessionUsage`/`sessionTokensConsumed` are cumulative BILLED throughput. Every turn resends the conversation, so the cumulative figure counts each turn’s prompt again in the next turn’s and is expected to dwarf the context size — that is not double-counting.',
|