graphlit-client 1.0.20260602001 → 1.0.20260605001
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/client.d.ts +11 -0
- package/dist/client.js +71 -29
- package/dist/helpers/stuck-detector.js +4 -3
- package/dist/types/agent.d.ts +7 -0
- package/dist/types/internal.d.ts +13 -0
- package/dist/types/internal.js +43 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -3250,6 +3250,17 @@ declare class Graphlit {
|
|
|
3250
3250
|
* with `intermediateMessages` rather than a single tool-call list.
|
|
3251
3251
|
*/
|
|
3252
3252
|
private detectTaskCompleteInMessages;
|
|
3253
|
+
/**
|
|
3254
|
+
* Treat a non-empty text-only turn as terminal when the caller explicitly
|
|
3255
|
+
* opts into interactive completion semantics.
|
|
3256
|
+
*/
|
|
3257
|
+
private detectTerminalTextCompletion;
|
|
3258
|
+
/**
|
|
3259
|
+
* Reconstruct terminal-text completion across resumed conversations. A
|
|
3260
|
+
* text-only assistant turn is considered terminal if it was not followed by
|
|
3261
|
+
* the harness' synthetic bare continuation prompt.
|
|
3262
|
+
*/
|
|
3263
|
+
private detectPersistedTurnCompletionReason;
|
|
3253
3264
|
/**
|
|
3254
3265
|
* Run LLM-as-judge quality assessment on a completed agent run.
|
|
3255
3266
|
*/
|
package/dist/client.js
CHANGED
|
@@ -11,7 +11,7 @@ import { getServiceType, getModelName, getModelEnum, isAnthropicAdaptiveThinking
|
|
|
11
11
|
import { StuckDetector } from "./helpers/stuck-detector.js";
|
|
12
12
|
import { TurnEvaluator } from "./helpers/turn-evaluator.js";
|
|
13
13
|
import { TokenBudgetTracker, truncateToolResult, windowToolRounds, estimateTokens, DEFAULT_CONTEXT_STRATEGY, } from "./helpers/context-management.js";
|
|
14
|
-
import { ProviderError } from "./types/internal.js";
|
|
14
|
+
import { isRetryableGraphQLTransportError, ProviderError, } from "./types/internal.js";
|
|
15
15
|
import { UIEventAdapter } from "./streaming/ui-event-adapter.js";
|
|
16
16
|
import { formatMessagesForOpenAI, formatMessagesForOpenAIResponsesInitialRound, extractInstructionsForOpenAIResponses, extractSystemInstructionParts, buildResponsesFunctionCallOutputItems, formatToolsForOpenAIResponses, formatMessagesForAnthropic, formatMessagesForGoogle, formatMessagesForMistral, formatMessagesForBedrock, } from "./streaming/llm-formatters.js";
|
|
17
17
|
import { streamWithOpenAIResponses, } from "./streaming/openai-responses.js";
|
|
@@ -528,7 +528,9 @@ class Graphlit {
|
|
|
528
528
|
}
|
|
529
529
|
refreshClient() {
|
|
530
530
|
this.client = undefined;
|
|
531
|
-
this.
|
|
531
|
+
if (this.jwtSecret) {
|
|
532
|
+
this.generateToken();
|
|
533
|
+
}
|
|
532
534
|
this.setupApolloClient();
|
|
533
535
|
}
|
|
534
536
|
setupApolloClient() {
|
|
@@ -548,28 +550,15 @@ class Graphlit {
|
|
|
548
550
|
// Check if we should retry this error
|
|
549
551
|
if (!error)
|
|
550
552
|
return false;
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
if (
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
error.statusCode;
|
|
559
|
-
// Check if status code is retryable
|
|
560
|
-
if (statusCode && this.retryConfig.retryableStatusCodes) {
|
|
561
|
-
const shouldRetry = this.retryConfig.retryableStatusCodes.includes(statusCode);
|
|
562
|
-
// Call onRetry callback if provided
|
|
563
|
-
if (shouldRetry &&
|
|
564
|
-
this.retryConfig.onRetry &&
|
|
565
|
-
_operation.getContext().retryCount !== undefined) {
|
|
566
|
-
const attempt = _operation.getContext().retryCount + 1;
|
|
567
|
-
this.retryConfig.onRetry(attempt, error, _operation);
|
|
568
|
-
}
|
|
569
|
-
return shouldRetry;
|
|
553
|
+
const shouldRetry = isRetryableGraphQLTransportError(error, this.retryConfig.retryableStatusCodes);
|
|
554
|
+
// Call onRetry callback if provided
|
|
555
|
+
if (shouldRetry &&
|
|
556
|
+
this.retryConfig.onRetry &&
|
|
557
|
+
_operation.getContext().retryCount !== undefined) {
|
|
558
|
+
const attempt = _operation.getContext().retryCount + 1;
|
|
559
|
+
this.retryConfig.onRetry(attempt, error, _operation);
|
|
570
560
|
}
|
|
571
|
-
|
|
572
|
-
return true;
|
|
561
|
+
return shouldRetry;
|
|
573
562
|
},
|
|
574
563
|
},
|
|
575
564
|
});
|
|
@@ -7233,12 +7222,14 @@ class Graphlit {
|
|
|
7233
7222
|
const turnResults = [];
|
|
7234
7223
|
let totalToolCalls = 0;
|
|
7235
7224
|
let status = "completed";
|
|
7225
|
+
let completionReason;
|
|
7236
7226
|
let finalMessage = "";
|
|
7237
7227
|
let stuckPattern;
|
|
7238
7228
|
let errorMessage;
|
|
7239
7229
|
let lastContextWindow;
|
|
7240
7230
|
let conversationId = options?.conversationId ?? "";
|
|
7241
7231
|
let resolvedAgentId = agentId ?? "";
|
|
7232
|
+
let resumedTurnCount = 0;
|
|
7242
7233
|
// Accumulate token usage across all turns from conversation_completed events
|
|
7243
7234
|
let accumulatedUsage = {
|
|
7244
7235
|
promptTokens: 0,
|
|
@@ -7424,6 +7415,7 @@ class Graphlit {
|
|
|
7424
7415
|
}
|
|
7425
7416
|
}
|
|
7426
7417
|
}
|
|
7418
|
+
resumedTurnCount = turnResults.length;
|
|
7427
7419
|
const fallbackSpecs = await this.resolveSpecificationReferences(fallbackReferences.filter((fallback) => fallback.id !== fullSpec?.id));
|
|
7428
7420
|
// ── Phase 2: The Loop ──────────────────────────────────────────────
|
|
7429
7421
|
let pendingStuckIntervention;
|
|
@@ -7643,6 +7635,7 @@ class Graphlit {
|
|
|
7643
7635
|
// records the terminal assistant/tool-call round, but does not execute
|
|
7644
7636
|
// the completion handler or start a follow-up filler round.
|
|
7645
7637
|
const taskCompleteThisTurn = this.detectTaskCompleteInMessages(loopResult.intermediateMessages);
|
|
7638
|
+
const terminalTextThisTurn = this.detectTerminalTextCompletion(loopResult, options?.completionMode);
|
|
7646
7639
|
const trimmedMessage = loopResult.finalAssistantMessage;
|
|
7647
7640
|
// 8b. Complete conversation (persist turn)
|
|
7648
7641
|
if (trimmedMessage) {
|
|
@@ -7701,6 +7694,11 @@ class Graphlit {
|
|
|
7701
7694
|
toolCallCount: loopResult.toolCallCount,
|
|
7702
7695
|
durationMs: turnDuration,
|
|
7703
7696
|
taskComplete: taskCompleteThisTurn,
|
|
7697
|
+
completionReason: taskCompleteThisTurn
|
|
7698
|
+
? "task_complete"
|
|
7699
|
+
: terminalTextThisTurn
|
|
7700
|
+
? "terminal_text"
|
|
7701
|
+
: undefined,
|
|
7704
7702
|
contextWindowUsage: loopResult.contextWindow,
|
|
7705
7703
|
contextActions: loopResult.contextActions.length > 0
|
|
7706
7704
|
? loopResult.contextActions
|
|
@@ -7718,9 +7716,10 @@ class Graphlit {
|
|
|
7718
7716
|
// 10. Notify callback
|
|
7719
7717
|
options?.onTurnComplete?.(turnResult);
|
|
7720
7718
|
// 11. Evaluate turn
|
|
7721
|
-
// a.
|
|
7722
|
-
if (
|
|
7719
|
+
// a. Terminal completion reached?
|
|
7720
|
+
if (turnResult.completionReason) {
|
|
7723
7721
|
status = "completed";
|
|
7722
|
+
completionReason = turnResult.completionReason;
|
|
7724
7723
|
break;
|
|
7725
7724
|
}
|
|
7726
7725
|
// b/c. Error handling
|
|
@@ -7762,9 +7761,9 @@ class Graphlit {
|
|
|
7762
7761
|
// status defaults to "completed", so we need a positive signal that
|
|
7763
7762
|
// task_complete was actually called — otherwise hitting the turn cap
|
|
7764
7763
|
// looks like success.
|
|
7765
|
-
const
|
|
7764
|
+
const runCompletedNaturally = turnResults.slice(resumedTurnCount).some((t) => t.taskComplete || t.completionReason === "terminal_text");
|
|
7766
7765
|
if (status === "completed" &&
|
|
7767
|
-
!
|
|
7766
|
+
!runCompletedNaturally &&
|
|
7768
7767
|
turnResults.length >= evaluator.adjustedMaxTurns) {
|
|
7769
7768
|
status = "budget_exhausted";
|
|
7770
7769
|
}
|
|
@@ -7780,6 +7779,7 @@ class Graphlit {
|
|
|
7780
7779
|
conversationId,
|
|
7781
7780
|
status,
|
|
7782
7781
|
finalMessage,
|
|
7782
|
+
completionReason,
|
|
7783
7783
|
turnResults,
|
|
7784
7784
|
totalToolCalls,
|
|
7785
7785
|
wallClockMs,
|
|
@@ -7805,6 +7805,7 @@ class Graphlit {
|
|
|
7805
7805
|
conversationId,
|
|
7806
7806
|
status,
|
|
7807
7807
|
finalMessage,
|
|
7808
|
+
completionReason,
|
|
7808
7809
|
turnResults,
|
|
7809
7810
|
totalToolCalls,
|
|
7810
7811
|
wallClockMs: Date.now() - runStart,
|
|
@@ -7878,8 +7879,9 @@ class Graphlit {
|
|
|
7878
7879
|
toolNames.push(tc.name);
|
|
7879
7880
|
}
|
|
7880
7881
|
}
|
|
7881
|
-
// Check tool response messages for errors
|
|
7882
|
+
// Check tool response messages for errors and locate the next user turn.
|
|
7882
7883
|
const toolErrors = [];
|
|
7884
|
+
let nextUserMsg;
|
|
7883
7885
|
for (let j = i + 1; j < messages.length; j++) {
|
|
7884
7886
|
const respMsg = messages[j];
|
|
7885
7887
|
if (respMsg.role === Types.ConversationRoleTypes.Tool) {
|
|
@@ -7888,9 +7890,11 @@ class Graphlit {
|
|
|
7888
7890
|
}
|
|
7889
7891
|
}
|
|
7890
7892
|
else if (respMsg.role === Types.ConversationRoleTypes.User) {
|
|
7893
|
+
nextUserMsg = respMsg;
|
|
7891
7894
|
break; // Next turn
|
|
7892
7895
|
}
|
|
7893
7896
|
}
|
|
7897
|
+
const taskComplete = this.detectTaskComplete(assistantMsg.toolCalls);
|
|
7894
7898
|
results.push({
|
|
7895
7899
|
turnNumber: turnNumber++,
|
|
7896
7900
|
prompt: msg.message || "",
|
|
@@ -7898,7 +7902,10 @@ class Graphlit {
|
|
|
7898
7902
|
toolCalls: [...new Set(toolNames)].sort(),
|
|
7899
7903
|
toolCallCount: toolNames.length,
|
|
7900
7904
|
durationMs: computePersistedToolRoundDurationMs(assistantMsg.toolCalls) ?? 0,
|
|
7901
|
-
taskComplete
|
|
7905
|
+
taskComplete,
|
|
7906
|
+
completionReason: taskComplete
|
|
7907
|
+
? "task_complete"
|
|
7908
|
+
: this.detectPersistedTurnCompletionReason(assistantMsg, nextUserMsg),
|
|
7902
7909
|
errors: toolErrors.length > 0 ? toolErrors : undefined,
|
|
7903
7910
|
});
|
|
7904
7911
|
}
|
|
@@ -8035,6 +8042,40 @@ class Graphlit {
|
|
|
8035
8042
|
}
|
|
8036
8043
|
return false;
|
|
8037
8044
|
}
|
|
8045
|
+
/**
|
|
8046
|
+
* Treat a non-empty text-only turn as terminal when the caller explicitly
|
|
8047
|
+
* opts into interactive completion semantics.
|
|
8048
|
+
*/
|
|
8049
|
+
detectTerminalTextCompletion(loopResult, completionMode) {
|
|
8050
|
+
if (completionMode !== "allow_terminal_text") {
|
|
8051
|
+
return false;
|
|
8052
|
+
}
|
|
8053
|
+
if (loopResult.toolCallCount !== 0) {
|
|
8054
|
+
return false;
|
|
8055
|
+
}
|
|
8056
|
+
if (loopResult.errors.length > 0) {
|
|
8057
|
+
return false;
|
|
8058
|
+
}
|
|
8059
|
+
return loopResult.finalAssistantMessage.trim().length > 0;
|
|
8060
|
+
}
|
|
8061
|
+
/**
|
|
8062
|
+
* Reconstruct terminal-text completion across resumed conversations. A
|
|
8063
|
+
* text-only assistant turn is considered terminal if it was not followed by
|
|
8064
|
+
* the harness' synthetic bare continuation prompt.
|
|
8065
|
+
*/
|
|
8066
|
+
detectPersistedTurnCompletionReason(assistantMessage, nextMessage) {
|
|
8067
|
+
if ((assistantMessage.message || "").trim().length === 0) {
|
|
8068
|
+
return undefined;
|
|
8069
|
+
}
|
|
8070
|
+
if ((assistantMessage.toolCalls?.length ?? 0) > 0) {
|
|
8071
|
+
return undefined;
|
|
8072
|
+
}
|
|
8073
|
+
if (nextMessage?.role === Types.ConversationRoleTypes.User &&
|
|
8074
|
+
(nextMessage.message || "").trim() === "Continue.") {
|
|
8075
|
+
return undefined;
|
|
8076
|
+
}
|
|
8077
|
+
return "terminal_text";
|
|
8078
|
+
}
|
|
8038
8079
|
/**
|
|
8039
8080
|
* Run LLM-as-judge quality assessment on a completed agent run.
|
|
8040
8081
|
*/
|
|
@@ -8114,6 +8155,7 @@ class Graphlit {
|
|
|
8114
8155
|
conversationId: params.conversationId,
|
|
8115
8156
|
status: params.status,
|
|
8116
8157
|
finalMessage: params.finalMessage,
|
|
8158
|
+
completionReason: params.completionReason,
|
|
8117
8159
|
turns: params.turnResults.length,
|
|
8118
8160
|
totalToolCalls: params.totalToolCalls,
|
|
8119
8161
|
wallClockMs: params.wallClockMs,
|
|
@@ -41,7 +41,7 @@ const STUCK_WINDOW = 5; // Number of recent turns to examine
|
|
|
41
41
|
const REPEAT_THRESHOLD = 3; // Minimum identical occurrences to detect a pattern
|
|
42
42
|
const SIMILARITY_THRESHOLD = 0.9; // Trigram similarity threshold for "same" response
|
|
43
43
|
const CONSECUTIVE_ERROR_THRESHOLD = 3; // Consecutive all-error turns
|
|
44
|
-
const CONSECUTIVE_EMPTY_THRESHOLD = 2; // Consecutive empty
|
|
44
|
+
const CONSECUTIVE_EMPTY_THRESHOLD = 2; // Consecutive empty non-terminal turns
|
|
45
45
|
/**
|
|
46
46
|
* Stateful detector that tracks patterns across harness turns and identifies
|
|
47
47
|
* when the agent is stuck in a loop.
|
|
@@ -69,7 +69,8 @@ export class StuckDetector {
|
|
|
69
69
|
const allErrors = turn.toolCallCount > 0 &&
|
|
70
70
|
(turn.errors?.length ?? 0) >= turn.toolCallCount;
|
|
71
71
|
this.errorHistory.push(allErrors);
|
|
72
|
-
|
|
72
|
+
const turnCompleted = turn.taskComplete || turn.completionReason != null;
|
|
73
|
+
if (turn.toolCallCount === 0 && !turnCompleted) {
|
|
73
74
|
this.emptyTurnCount++;
|
|
74
75
|
}
|
|
75
76
|
else {
|
|
@@ -113,7 +114,7 @@ export class StuckDetector {
|
|
|
113
114
|
return this.strike("error_loop");
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
|
-
// 4. Empty turns — 2+ consecutive turns with 0 tool calls and no
|
|
117
|
+
// 4. Empty turns — 2+ consecutive turns with 0 tool calls and no terminal completion
|
|
117
118
|
if (this.emptyTurnCount >= CONSECUTIVE_EMPTY_THRESHOLD) {
|
|
118
119
|
return this.strike("empty_turns");
|
|
119
120
|
}
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -118,6 +118,10 @@ export interface AgentError {
|
|
|
118
118
|
}
|
|
119
119
|
/** Terminal status of a runAgent execution. */
|
|
120
120
|
export type HarnessStatus = "completed" | "budget_exhausted" | "stuck" | "error" | "cancelled";
|
|
121
|
+
/** How a runAgent execution reached terminal completion. */
|
|
122
|
+
export type RunCompletionReason = "task_complete" | "terminal_text";
|
|
123
|
+
/** Policy controlling when text-only turns should be treated as terminal. */
|
|
124
|
+
export type RunCompletionMode = "task_complete_only" | "allow_terminal_text";
|
|
121
125
|
/** Per-turn data captured by the harness. */
|
|
122
126
|
export interface TurnResult {
|
|
123
127
|
turnNumber: number;
|
|
@@ -127,6 +131,7 @@ export interface TurnResult {
|
|
|
127
131
|
toolCallCount: number;
|
|
128
132
|
durationMs: number;
|
|
129
133
|
taskComplete?: boolean;
|
|
134
|
+
completionReason?: RunCompletionReason;
|
|
130
135
|
contextWindowUsage?: ContextWindowUsage;
|
|
131
136
|
contextActions?: ContextManagementAction[];
|
|
132
137
|
errors?: string[];
|
|
@@ -161,6 +166,7 @@ export interface RunAgentOptions {
|
|
|
161
166
|
maxWallClockMs?: number;
|
|
162
167
|
maxToolCalls?: number;
|
|
163
168
|
windDownTurns?: number;
|
|
169
|
+
completionMode?: RunCompletionMode;
|
|
164
170
|
onTurnComplete?: (turn: TurnResult) => void;
|
|
165
171
|
onBudgetWarning?: (snapshot: BudgetSnapshot) => void;
|
|
166
172
|
onStreamEvent?: (event: AgentStreamEvent) => void;
|
|
@@ -193,6 +199,7 @@ export interface RunAgentResult {
|
|
|
193
199
|
conversationId: string;
|
|
194
200
|
status: HarnessStatus;
|
|
195
201
|
finalMessage: string;
|
|
202
|
+
completionReason?: RunCompletionReason;
|
|
196
203
|
turns: number;
|
|
197
204
|
totalToolCalls: number;
|
|
198
205
|
wallClockMs: number;
|
package/dist/types/internal.d.ts
CHANGED
|
@@ -117,6 +117,19 @@ export declare class ProviderError extends Error {
|
|
|
117
117
|
* Used as a catch-all after provider-specific error classification.
|
|
118
118
|
*/
|
|
119
119
|
export declare function isRetryableServerError(error: any): boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Extract an HTTP status code from common Apollo/network error shapes.
|
|
122
|
+
*/
|
|
123
|
+
export declare function getErrorStatusCode(error: any): number | undefined;
|
|
124
|
+
/**
|
|
125
|
+
* Detect retryable GraphQL transport errors from Apollo RetryLink.
|
|
126
|
+
*
|
|
127
|
+
* RetryLink can receive either:
|
|
128
|
+
* - an ApolloError with `networkError`
|
|
129
|
+
* - a raw HttpLink `ServerError` with `statusCode` / `response.status`
|
|
130
|
+
* - a nested network/cause error from undici/fetch
|
|
131
|
+
*/
|
|
132
|
+
export declare function isRetryableGraphQLTransportError(error: any, retryableStatusCodes?: number[]): boolean;
|
|
120
133
|
/**
|
|
121
134
|
* Detect rate-limit / overloaded errors across providers.
|
|
122
135
|
*/
|
package/dist/types/internal.js
CHANGED
|
@@ -37,6 +37,49 @@ export function isRetryableServerError(error) {
|
|
|
37
37
|
msg.includes("bad gateway") ||
|
|
38
38
|
msg.includes("gateway timeout"));
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Extract an HTTP status code from common Apollo/network error shapes.
|
|
42
|
+
*/
|
|
43
|
+
export function getErrorStatusCode(error) {
|
|
44
|
+
if (!error || typeof error !== "object")
|
|
45
|
+
return undefined;
|
|
46
|
+
const status = typeof error.status === "number"
|
|
47
|
+
? error.status
|
|
48
|
+
: typeof error.statusCode === "number"
|
|
49
|
+
? error.statusCode
|
|
50
|
+
: undefined;
|
|
51
|
+
if (typeof status === "number")
|
|
52
|
+
return status;
|
|
53
|
+
const responseStatus = typeof error.response?.status === "number"
|
|
54
|
+
? error.response.status
|
|
55
|
+
: undefined;
|
|
56
|
+
if (typeof responseStatus === "number")
|
|
57
|
+
return responseStatus;
|
|
58
|
+
return (getErrorStatusCode(error.networkError) ?? getErrorStatusCode(error.cause));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Detect retryable GraphQL transport errors from Apollo RetryLink.
|
|
62
|
+
*
|
|
63
|
+
* RetryLink can receive either:
|
|
64
|
+
* - an ApolloError with `networkError`
|
|
65
|
+
* - a raw HttpLink `ServerError` with `statusCode` / `response.status`
|
|
66
|
+
* - a nested network/cause error from undici/fetch
|
|
67
|
+
*/
|
|
68
|
+
export function isRetryableGraphQLTransportError(error, retryableStatusCodes = [429, 500, 502, 503, 504]) {
|
|
69
|
+
if (!error)
|
|
70
|
+
return false;
|
|
71
|
+
const statusCode = getErrorStatusCode(error);
|
|
72
|
+
if (typeof statusCode === "number") {
|
|
73
|
+
return retryableStatusCodes.includes(statusCode);
|
|
74
|
+
}
|
|
75
|
+
if (Array.isArray(error.graphQLErrors) && error.graphQLErrors.length > 0) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return (!!error.networkError ||
|
|
79
|
+
isNetworkError(error) ||
|
|
80
|
+
isRetryableServerError(error) ||
|
|
81
|
+
isRetryableGraphQLTransportError(error.cause, retryableStatusCodes));
|
|
82
|
+
}
|
|
40
83
|
/**
|
|
41
84
|
* Detect rate-limit / overloaded errors across providers.
|
|
42
85
|
*/
|