graphlit-client 1.0.20260420001 → 1.0.20260420002

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 CHANGED
@@ -3096,6 +3096,11 @@ declare class Graphlit {
3096
3096
  * Tool calls on assistant messages are extracted for stuck-pattern tracking.
3097
3097
  */
3098
3098
  private reconstructTurnResults;
3099
+ /**
3100
+ * Detect whether one tool call is a task_complete invocation, either
3101
+ * directly or through a meta-executor such as execute_tool.
3102
+ */
3103
+ private isTaskCompleteToolCall;
3099
3104
  /**
3100
3105
  * Detect whether task_complete was invoked in a single list of tool calls.
3101
3106
  *
@@ -3119,32 +3124,6 @@ declare class Graphlit {
3119
3124
  * with `intermediateMessages` rather than a single tool-call list.
3120
3125
  */
3121
3126
  private detectTaskCompleteInMessages;
3122
- /**
3123
- * Extract the `final_message` argument from a task_complete invocation
3124
- * within a list of tool calls. Handles both direct invocation and
3125
- * meta-executor wrapping (mirrors `detectTaskComplete`).
3126
- *
3127
- * Returns undefined if task_complete was not called, the arg is missing,
3128
- * or the arg is not a non-empty string.
3129
- */
3130
- private extractTaskCompleteFinalMessage;
3131
- /**
3132
- * Scan a list of messages for a task_complete invocation's `final_message`
3133
- * arg. Convenience wrapper over `extractTaskCompleteFinalMessage`.
3134
- */
3135
- private extractTaskCompleteFinalMessageFromMessages;
3136
- /**
3137
- * Sanitize task_complete's `final_message` payload out of persisted
3138
- * intermediate messages. The tool call record (name, id, timing, ordering)
3139
- * is preserved as a completion marker, but its `final_message` arg is
3140
- * stripped because that content is already stored as the turn's assistant
3141
- * text via `completion`. Keeping the tool call without the payload yields
3142
- * the same storage win as full removal while preserving the trace.
3143
- *
3144
- * Handles both direct and meta-executor (`execute_tool({tool: ...})`)
3145
- * invocations, matching `detectTaskComplete`.
3146
- */
3147
- private sanitizeTaskCompletePayload;
3148
3127
  /**
3149
3128
  * Run LLM-as-judge quality assessment on a completed agent run.
3150
3129
  */
package/dist/client.js CHANGED
@@ -3059,7 +3059,11 @@ class Graphlit {
3059
3059
  * @returns The result of the deletion.
3060
3060
  */
3061
3061
  async deleteAllSkills(filter, isSynchronous, correlationId) {
3062
- return this.mutateAndCheckError(Documents.DeleteAllSkills, { filter: filter, isSynchronous: isSynchronous, correlationId: correlationId });
3062
+ return this.mutateAndCheckError(Documents.DeleteAllSkills, {
3063
+ filter: filter,
3064
+ isSynchronous: isSynchronous,
3065
+ correlationId: correlationId,
3066
+ });
3063
3067
  }
3064
3068
  /**
3065
3069
  * Enables a skill.
@@ -3111,7 +3115,10 @@ class Graphlit {
3111
3115
  * @returns The updated collections.
3112
3116
  */
3113
3117
  async addSkillsToCollections(skills, collections) {
3114
- return this.mutateAndCheckError(Documents.AddSkillsToCollections, { skills: skills, collections: collections });
3118
+ return this.mutateAndCheckError(Documents.AddSkillsToCollections, {
3119
+ skills: skills,
3120
+ collections: collections,
3121
+ });
3115
3122
  }
3116
3123
  /**
3117
3124
  * Removes skills from a collection.
@@ -3120,7 +3127,10 @@ class Graphlit {
3120
3127
  * @returns The updated collection.
3121
3128
  */
3122
3129
  async removeSkillsFromCollection(skills, collection) {
3123
- return this.mutateAndCheckError(Documents.RemoveSkillsFromCollection, { skills: skills, collection: collection });
3130
+ return this.mutateAndCheckError(Documents.RemoveSkillsFromCollection, {
3131
+ skills: skills,
3132
+ collection: collection,
3133
+ });
3124
3134
  }
3125
3135
  /**
3126
3136
  * Prompts multiple specifications and returns the best response.
@@ -5730,7 +5740,8 @@ class Graphlit {
5730
5740
  });
5731
5741
  }
5732
5742
  // Emit tool events if there were tool calls
5733
- if (promptResult.toolResults && promptResult.toolResults.length > 0) {
5743
+ if (promptResult.toolResults &&
5744
+ promptResult.toolResults.length > 0) {
5734
5745
  for (const toolResult of promptResult.toolResults) {
5735
5746
  const toolCall = buildConversationToolCallFromResult(toolResult);
5736
5747
  onEvent({
@@ -6039,10 +6050,7 @@ class Graphlit {
6039
6050
  completionInput.thinkingSignature =
6040
6051
  loopResult.lastRoundReasoning.signature;
6041
6052
  }
6042
- finalMessageInputs = [
6043
- ...(finalMessageInputs || []),
6044
- completionInput,
6045
- ];
6053
+ finalMessageInputs = [...(finalMessageInputs || []), completionInput];
6046
6054
  }
6047
6055
  const completeResponse = await this.completeConversation(trimmedMessage, conversationId, millisecondsToTimeSpan(completionTime), millisecondsToTimeSpan(ttft), throughput, collectedArtifacts.length > 0 ? collectedArtifacts : undefined, finalMessageInputs, correlationId);
6048
6056
  finalTokens =
@@ -6063,7 +6071,7 @@ class Graphlit {
6063
6071
  * Shared between streamAgent (single turn) and runAgent (multi-turn harness).
6064
6072
  */
6065
6073
  async executeStreamingLoop(config) {
6066
- const { conversationId, specification, tools, toolHandlers, uiAdapter, budgetTracker, contextStrategy: { toolResultTokenLimit, toolRoundLimit, rebudgetThreshold }, maxRounds, abortSignal, useResponsesApi, mimeType, data, correlationId, persona, } = config;
6074
+ const { conversationId, specification, tools, toolHandlers, uiAdapter, budgetTracker, contextStrategy: { toolResultTokenLimit, toolRoundLimit, rebudgetThreshold, }, maxRounds, abortSignal, useResponsesApi, mimeType, data, correlationId, persona, } = config;
6067
6075
  let messages = config.messages;
6068
6076
  let currentRound = 0;
6069
6077
  let fullMessage = "";
@@ -6176,7 +6184,7 @@ class Graphlit {
6176
6184
  if (process.env.DEBUG_GRAPHLIT_SDK_STREAMING_MESSAGES) {
6177
6185
  console.log(`🔍 [OpenAI Responses] Sending ${openAIResponsesState.initialInput.length} initial items and ${openAIResponsesState.continuationItems.length} continuation items`);
6178
6186
  }
6179
- const responsesResult = await this.streamWithOpenAIResponses(specification, messages, openAIResponsesPendingToolMessages, tools, uiAdapter, abortSignal, openAIResponsesState, (currentRound === 0 && tools?.length) ? "required" : undefined);
6187
+ const responsesResult = await this.streamWithOpenAIResponses(specification, messages, openAIResponsesPendingToolMessages, tools, uiAdapter, abortSignal, openAIResponsesState, currentRound === 0 && tools?.length ? "required" : undefined);
6180
6188
  roundMessage = responsesResult.message;
6181
6189
  toolCalls = responsesResult.toolCalls;
6182
6190
  openAIResponsesState = responsesResult.state;
@@ -6314,7 +6322,10 @@ class Graphlit {
6314
6322
  }
6315
6323
  const mistralMessages = formatMessagesForMistral(messages);
6316
6324
  // ALWAYS log when there's a tool-related issue for debugging
6317
- const hasToolCalls = mistralMessages.some((m) => "tool_calls" in m && Array.isArray(m.tool_calls) && m.tool_calls.length > 0);
6325
+ const hasToolCalls = mistralMessages.some((m) => "tool_calls" in m &&
6326
+ Array.isArray(m.tool_calls) &&
6327
+ m
6328
+ .tool_calls.length > 0);
6318
6329
  const hasToolResponses = mistralMessages.some((m) => m.role === "tool");
6319
6330
  // Count tool responses to determine if we should pass tools
6320
6331
  const toolResponseCount = mistralMessages.filter((m) => m.role === "tool").length;
@@ -6325,7 +6336,8 @@ class Graphlit {
6325
6336
  console.log(JSON.stringify(mistralMessages, null, 2));
6326
6337
  // Count tool calls and responses
6327
6338
  const toolCallCount = mistralMessages.reduce((count, m) => {
6328
- const calls = m.tool_calls;
6339
+ const calls = m
6340
+ .tool_calls;
6329
6341
  return count + (Array.isArray(calls) ? calls.length : 0);
6330
6342
  }, 0);
6331
6343
  console.log(`🔍 [Mistral] Tool calls: ${toolCallCount}, Tool responses: ${toolResponseCount}`);
@@ -6432,13 +6444,11 @@ class Graphlit {
6432
6444
  if (abortSignal?.aborted)
6433
6445
  throw retryError;
6434
6446
  const isRetryable = retryError instanceof ProviderError && retryError.retryable;
6435
- if (!isRetryable ||
6436
- providerAttempt >= DEFAULT_PROVIDER_RETRIES) {
6447
+ if (!isRetryable || providerAttempt >= DEFAULT_PROVIDER_RETRIES) {
6437
6448
  throw retryError;
6438
6449
  }
6439
6450
  // Exponential backoff with jitter
6440
- const delay = Math.min(PROVIDER_RETRY_BASE_DELAY_MS *
6441
- Math.pow(2, providerAttempt), PROVIDER_RETRY_MAX_DELAY_MS);
6451
+ const delay = Math.min(PROVIDER_RETRY_BASE_DELAY_MS * Math.pow(2, providerAttempt), PROVIDER_RETRY_MAX_DELAY_MS);
6442
6452
  const jitter = Math.random() * delay * 0.1;
6443
6453
  const totalDelay = Math.round(delay + jitter);
6444
6454
  if (process.env.DEBUG_GRAPHLIT_SDK_STREAMING) {
@@ -6467,42 +6477,61 @@ class Graphlit {
6467
6477
  if (!toolCalls || toolCalls.length === 0) {
6468
6478
  break;
6469
6479
  }
6470
- // Execute tools and prepare for next round
6471
- if (toolHandlers && toolCalls.length > 0) {
6472
- if (process.env.DEBUG_GRAPHLIT_SDK_STREAMING) {
6473
- console.log(`\n🔧 [executeStreamingLoop] Round ${currentRound}: Processing ${toolCalls.length} tool calls`);
6474
- toolCalls.forEach((tc, idx) => {
6475
- console.log(` ${idx + 1}. ${tc.name} (${tc.id}) - Args length: ${tc.arguments.length}`);
6476
- });
6480
+ if (process.env.DEBUG_GRAPHLIT_SDK_STREAMING) {
6481
+ console.log(`\n🔧 [executeStreamingLoop] Round ${currentRound}: Processing ${toolCalls.length} tool calls`);
6482
+ toolCalls.forEach((tc, idx) => {
6483
+ console.log(` ${idx + 1}. ${tc.name} (${tc.id}) - Args length: ${tc.arguments.length}`);
6484
+ });
6485
+ }
6486
+ // Add assistant message with tool calls to conversation. This is done
6487
+ // before terminal task_complete detection so the outer runAgent loop can
6488
+ // still observe the completion signal in intermediateMessages.
6489
+ const roundToolCalls = toolCalls.map(normalizeToolCallForExecution);
6490
+ const assistantMessage = {
6491
+ __typename: "ConversationMessage",
6492
+ role: Types.ConversationRoleTypes.Assistant,
6493
+ message: roundMessage,
6494
+ toolCalls: roundToolCalls,
6495
+ timestamp: new Date().toISOString(),
6496
+ };
6497
+ if (roundReasoning) {
6498
+ assistantMessage.thinkingContent = roundReasoning.content;
6499
+ if (roundReasoning.signature) {
6500
+ assistantMessage.thinkingSignature = roundReasoning.signature;
6477
6501
  }
6478
- // Add assistant message with tool calls to conversation
6479
- const roundToolCalls = toolCalls.map(normalizeToolCallForExecution);
6480
- const assistantMessage = {
6481
- __typename: "ConversationMessage",
6482
- role: Types.ConversationRoleTypes.Assistant,
6483
- message: roundMessage,
6484
- toolCalls: roundToolCalls,
6485
- timestamp: new Date().toISOString(),
6486
- };
6487
- if (roundReasoning) {
6488
- assistantMessage.thinkingContent = roundReasoning.content;
6489
- if (roundReasoning.signature) {
6490
- assistantMessage.thinkingSignature = roundReasoning.signature;
6502
+ reasoningByMessageIndex.set(messages.length, roundReasoning);
6503
+ }
6504
+ messages.push(assistantMessage);
6505
+ // Track tool names for stuck detection
6506
+ for (const tc of roundToolCalls) {
6507
+ toolCallNames.push(tc.name);
6508
+ }
6509
+ totalToolCallCount += roundToolCalls.length;
6510
+ // Track assistant message in budget
6511
+ if (budgetTracker) {
6512
+ const assistantTokens = estimateTokens(roundMessage) +
6513
+ roundToolCalls.reduce((sum, tc) => sum + estimateTokens(tc.arguments), 0);
6514
+ budgetTracker.addMessage("", assistantTokens);
6515
+ }
6516
+ // task_complete is terminal. Do not invoke its handler and feed an ack
6517
+ // back to the model; that extra round can produce trailing filler which
6518
+ // overwrites finalAssistantMessage.
6519
+ if (this.detectTaskComplete(roundToolCalls)) {
6520
+ const terminalAt = nowIsoString();
6521
+ for (const toolCall of roundToolCalls) {
6522
+ if (this.isTaskCompleteToolCall(toolCall)) {
6523
+ toolCall.startedAt = toolCall.startedAt ?? terminalAt;
6524
+ toolCall.completedAt = toolCall.completedAt ?? terminalAt;
6525
+ toolCall.durationMs = toolCall.durationMs ?? 0;
6526
+ toolCall.firstStatusAt =
6527
+ toolCall.firstStatusAt ?? toolCall.startedAt;
6528
+ toolCall.status = Types.ToolExecutionStatus.Completed;
6491
6529
  }
6492
- reasoningByMessageIndex.set(messages.length, roundReasoning);
6493
- }
6494
- messages.push(assistantMessage);
6495
- // Track tool names for stuck detection
6496
- for (const tc of roundToolCalls) {
6497
- toolCallNames.push(tc.name);
6498
- }
6499
- totalToolCallCount += roundToolCalls.length;
6500
- // Track assistant message in budget
6501
- if (budgetTracker) {
6502
- const assistantTokens = estimateTokens(roundMessage) +
6503
- roundToolCalls.reduce((sum, tc) => sum + estimateTokens(tc.arguments), 0);
6504
- budgetTracker.addMessage("", assistantTokens);
6505
6530
  }
6531
+ break;
6532
+ }
6533
+ // Execute tools and prepare for next round
6534
+ if (toolHandlers) {
6506
6535
  const toolExecutionResults = new Array(roundToolCalls.length);
6507
6536
  const executeStreamingToolCall = async (toolCall, index) => {
6508
6537
  if (abortSignal?.aborted) {
@@ -6516,7 +6545,8 @@ class Graphlit {
6516
6545
  toolCall.completedAt = terminalAt;
6517
6546
  toolCall.durationMs = 0;
6518
6547
  toolCall.failedAt = terminalAt;
6519
- toolCall.firstStatusAt = toolCall.firstStatusAt ?? toolCall.startedAt;
6548
+ toolCall.firstStatusAt =
6549
+ toolCall.firstStatusAt ?? toolCall.startedAt;
6520
6550
  toolCall.status = Types.ToolExecutionStatus.Failed;
6521
6551
  uiAdapter.handleEvent({
6522
6552
  type: "tool_call_complete",
@@ -6611,7 +6641,8 @@ class Graphlit {
6611
6641
  toolCall.completedAt = terminalAt;
6612
6642
  toolCall.durationMs = 0;
6613
6643
  toolCall.failedAt = terminalAt;
6614
- toolCall.firstStatusAt = toolCall.firstStatusAt ?? toolCall.startedAt;
6644
+ toolCall.firstStatusAt =
6645
+ toolCall.firstStatusAt ?? toolCall.startedAt;
6615
6646
  toolCall.status = Types.ToolExecutionStatus.Failed;
6616
6647
  uiAdapter.handleEvent({
6617
6648
  type: "tool_call_complete",
@@ -6647,7 +6678,8 @@ class Graphlit {
6647
6678
  const executionStartMs = Date.now();
6648
6679
  const executionStartedAt = new Date(executionStartMs).toISOString();
6649
6680
  toolCall.startedAt = executionStartedAt;
6650
- toolCall.firstStatusAt = toolCall.firstStatusAt ?? executionStartedAt;
6681
+ toolCall.firstStatusAt =
6682
+ toolCall.firstStatusAt ?? executionStartedAt;
6651
6683
  uiAdapter.handleEvent({
6652
6684
  type: "tool_call_executing",
6653
6685
  toolCall: {
@@ -6717,7 +6749,8 @@ class Graphlit {
6717
6749
  toolCall.durationMs = Math.max(0, new Date(completedAt).getTime() -
6718
6750
  new Date(toolCall.startedAt).getTime());
6719
6751
  toolCall.failedAt = completedAt;
6720
- toolCall.firstStatusAt = toolCall.firstStatusAt ?? toolCall.startedAt;
6752
+ toolCall.firstStatusAt =
6753
+ toolCall.firstStatusAt ?? toolCall.startedAt;
6721
6754
  toolCall.status = Types.ToolExecutionStatus.Failed;
6722
6755
  uiAdapter.handleEvent({
6723
6756
  type: "tool_call_complete",
@@ -6891,7 +6924,7 @@ class Graphlit {
6891
6924
  let lastTurnUsage;
6892
6925
  // Wrap onEvent to capture per-turn usage from conversation_completed events
6893
6926
  const wrappedOnEvent = (event) => {
6894
- if (event.type === 'conversation_completed' && event.usage) {
6927
+ if (event.type === "conversation_completed" && event.usage) {
6895
6928
  const u = event.usage;
6896
6929
  lastTurnUsage = {
6897
6930
  promptTokens: u.promptTokens || 0,
@@ -6960,7 +6993,8 @@ class Graphlit {
6960
6993
  }
6961
6994
  // Get full specification for streaming support check and complexity classification
6962
6995
  const fullSpec = agentSpec?.id
6963
- ? (await this.getSpecification(agentSpec.id)).specification
6996
+ ? (await this.getSpecification(agentSpec.id))
6997
+ .specification
6964
6998
  : undefined;
6965
6999
  // Initialize evaluator and stuck detector
6966
7000
  const evaluator = new TurnEvaluator({
@@ -6973,40 +7007,36 @@ class Graphlit {
6973
7007
  const stuckDetector = new StuckDetector();
6974
7008
  // Adaptive budget: classify task complexity via LLM
6975
7009
  if (fullSpec) {
6976
- const { multiplier } = await evaluator.classifyComplexity(prompt, (classPrompt, text, classTools) => this.extractText(classPrompt, text, classTools, undefined, undefined, options?.correlationId)
6977
- .then((r) => r.extractText));
7010
+ const { multiplier } = await evaluator.classifyComplexity(prompt, (classPrompt, text, classTools) => this.extractText(classPrompt, text, classTools, undefined, undefined, options?.correlationId).then((r) => r.extractText));
6978
7011
  if (multiplier > 1.0) {
6979
7012
  evaluator.adjustBudget(multiplier);
6980
7013
  }
6981
7014
  }
6982
7015
  // Build tools list with task_complete prepended.
6983
7016
  //
6984
- // task_complete's optional `final_message` parameter is the explicit
6985
- // commitment of the agent's user-facing answer. When provided, it
6986
- // becomes the persisted assistant message for that turn AND the run's
6987
- // `finalMessage`, overriding any prose the model wrote around the call.
7017
+ // task_complete is a bare completion signal — it takes no arguments.
7018
+ // The agent's user-facing answer MUST be in its final assistant message
7019
+ // (the `fullMessage` / `finalAssistantMessage` on the turn result); this
7020
+ // tool only marks the run as done.
6988
7021
  //
6989
- // An earlier version used a `summary` field. The name encouraged
6990
- // truncated summarization the model would stuff a short summary into
6991
- // the arg and emit useless prose like "answer delivered above". The
6992
- // field name `final_message` and description below are explicit: this
6993
- // IS the full answer, not a summary, and it replaces surrounding prose.
7022
+ // The streaming loop treats task_complete as TERMINAL: when a round's
7023
+ // tool calls include task_complete, the loop breaks immediately without
7024
+ // invoking the handler or starting another round. That prevents the
7025
+ // model from emitting trailing filler ("delivered above", "see the
7026
+ // response") after the tool-call ack, which would otherwise overwrite
7027
+ // the real answer in `finalAssistantMessage`.
6994
7028
  //
6995
- // If the model omits `final_message`, the SDK falls back to the last
6996
- // turn whose assistant text was substantive AND did not itself call
6997
- // task_complete so a throwaway completion turn can't overwrite the
6998
- // real answer in a prior turn.
7029
+ // Earlier versions had a `summary` field. That consistently caused the
7030
+ // model to stuff its answer into the tool argument and then write a
7031
+ // useless prose message ("answer delivered above") the user would see
7032
+ // the empty prose, not the answer. Removing the field eliminates the
7033
+ // hiding place.
6999
7034
  const taskCompleteTool = {
7000
7035
  name: "task_complete",
7001
- description: "Signal that you have completed the assigned task. Provide your complete, final user-facing answer as `final_message` full prose, not a summary. That string becomes the assistant message the user sees; do not also emit the answer as plain text around this call. Calling this tool ends the run.",
7036
+ description: "Signal that you have completed the assigned task. Your full answer must already be in your assistant message BEFORE calling this that prose is what the user sees. This tool takes no arguments; it only ends the run.",
7002
7037
  schema: JSON.stringify({
7003
7038
  type: "object",
7004
- properties: {
7005
- final_message: {
7006
- type: "string",
7007
- description: "Your complete, final answer to the user as full prose. Replaces any assistant text written around this tool call.",
7008
- },
7009
- },
7039
+ properties: {},
7010
7040
  }),
7011
7041
  };
7012
7042
  const allTools = [
@@ -7172,8 +7202,7 @@ class Graphlit {
7172
7202
  messageToAdd.toolCallResponse =
7173
7203
  historyMessage.toolCallResponse;
7174
7204
  if (historyMessage.thinkingContent)
7175
- messageToAdd.thinkingContent =
7176
- historyMessage.thinkingContent;
7205
+ messageToAdd.thinkingContent = historyMessage.thinkingContent;
7177
7206
  if (historyMessage.thinkingSignature)
7178
7207
  messageToAdd.thinkingSignature =
7179
7208
  historyMessage.thinkingSignature;
@@ -7264,39 +7293,13 @@ class Graphlit {
7264
7293
  finally {
7265
7294
  uiAdapter.dispose();
7266
7295
  }
7267
- // 8. Detect task_complete and extract its optional `final_message`
7268
- // arg BEFORE persistence. When provided, it replaces the turn's
7269
- // assistant prose as the canonical answer see the task_complete
7270
- // tool schema above.
7296
+ // 8. Detect task_complete before persistence. The streaming loop
7297
+ // records the terminal assistant/tool-call round, but does not execute
7298
+ // the completion handler or start a follow-up filler round.
7271
7299
  const taskCompleteThisTurn = this.detectTaskCompleteInMessages(loopResult.intermediateMessages);
7272
- const taskCompleteFinalMessage = taskCompleteThisTurn
7273
- ? this.extractTaskCompleteFinalMessageFromMessages(loopResult.intermediateMessages)
7274
- : undefined;
7275
- // Resolve the assistant message to persist for this turn:
7276
- // - task_complete({final_message}) → final_message (authoritative)
7277
- // - task_complete() with no arg, prior substantive answer exists →
7278
- // drop the turn's prose so throwaway text like "delivered above"
7279
- // doesn't pollute chat history
7280
- // - task_complete() with no arg, no prior answer → keep the prose
7281
- // (single-shot completion case)
7282
- // - normal turn → keep the prose
7283
- let assistantMessageToPersist;
7284
- if (taskCompleteThisTurn) {
7285
- if (taskCompleteFinalMessage) {
7286
- assistantMessageToPersist = taskCompleteFinalMessage;
7287
- }
7288
- else if (finalMessage) {
7289
- assistantMessageToPersist = "";
7290
- }
7291
- else {
7292
- assistantMessageToPersist = loopResult.finalAssistantMessage;
7293
- }
7294
- }
7295
- else {
7296
- assistantMessageToPersist = loopResult.finalAssistantMessage;
7297
- }
7300
+ const trimmedMessage = loopResult.finalAssistantMessage;
7298
7301
  // 8b. Complete conversation (persist turn)
7299
- if (assistantMessageToPersist) {
7302
+ if (trimmedMessage) {
7300
7303
  const completionTime = uiAdapter.getCompletionTime();
7301
7304
  const ttft = uiAdapter.getTTFT();
7302
7305
  const throughput = uiAdapter.getThroughput();
@@ -7305,20 +7308,13 @@ class Graphlit {
7305
7308
  return undefined;
7306
7309
  return `PT${ms / 1000}S`;
7307
7310
  };
7308
- // Sanitize task_complete's final_message arg before persistence —
7309
- // the content is already saved as the turn's assistant text, so
7310
- // keeping it in the tool-call arguments would double-store it.
7311
- // The tool call record itself stays as a completion marker.
7312
- const sanitizedIntermediates = taskCompleteThisTurn
7313
- ? this.sanitizeTaskCompletePayload(loopResult.intermediateMessages)
7314
- : loopResult.intermediateMessages;
7315
- let turnMessageInputs = sanitizedIntermediates.length > 0
7316
- ? sanitizedIntermediates
7311
+ let turnMessageInputs = loopResult.intermediateMessages.length > 0
7312
+ ? loopResult.intermediateMessages
7317
7313
  : undefined;
7318
7314
  if (loopResult.lastRoundReasoning) {
7319
7315
  const completionInput = {
7320
7316
  role: Types.ConversationRoleTypes.Assistant,
7321
- message: assistantMessageToPersist,
7317
+ message: trimmedMessage,
7322
7318
  timestamp: new Date().toISOString(),
7323
7319
  thinkingContent: loopResult.lastRoundReasoning.content,
7324
7320
  };
@@ -7326,12 +7322,9 @@ class Graphlit {
7326
7322
  completionInput.thinkingSignature =
7327
7323
  loopResult.lastRoundReasoning.signature;
7328
7324
  }
7329
- turnMessageInputs = [
7330
- ...(turnMessageInputs || []),
7331
- completionInput,
7332
- ];
7325
+ turnMessageInputs = [...(turnMessageInputs || []), completionInput];
7333
7326
  }
7334
- await this.completeConversation(assistantMessageToPersist, conversationId, millisecondsToTimeSpan(completionTime), millisecondsToTimeSpan(ttft), throughput, undefined, turnMessageInputs, options?.correlationId);
7327
+ await this.completeConversation(trimmedMessage, conversationId, millisecondsToTimeSpan(completionTime), millisecondsToTimeSpan(ttft), throughput, undefined, turnMessageInputs, options?.correlationId);
7335
7328
  // Emit completion event
7336
7329
  uiAdapter.handleEvent({
7337
7330
  type: "complete",
@@ -7344,7 +7337,7 @@ class Graphlit {
7344
7337
  const turnResult = {
7345
7338
  turnNumber: turn,
7346
7339
  prompt: currentPrompt,
7347
- responseText: taskCompleteFinalMessage || loopResult.fullMessage,
7340
+ responseText: loopResult.fullMessage,
7348
7341
  toolCalls: turnToolNames,
7349
7342
  toolCallCount: loopResult.toolCallCount,
7350
7343
  durationMs: turnDuration,
@@ -7360,25 +7353,8 @@ class Graphlit {
7360
7353
  lastTurnUsage = undefined;
7361
7354
  turnResults.push(turnResult);
7362
7355
  totalToolCalls += loopResult.toolCallCount;
7363
- // Update finalMessage with priority:
7364
- // - task_complete({final_message}) → authoritative
7365
- // - task_complete() without arg → keep prior finalMessage (avoids
7366
- // throwaway completion-turn prose overwriting a real answer)
7367
- // - normal turn → this turn's assistant text
7368
- if (taskCompleteThisTurn) {
7369
- if (taskCompleteFinalMessage) {
7370
- finalMessage = taskCompleteFinalMessage;
7371
- }
7372
- else if (!finalMessage) {
7373
- // No prior answer — this turn's prose is all we have
7374
- finalMessage =
7375
- loopResult.finalAssistantMessage || loopResult.fullMessage;
7376
- }
7377
- }
7378
- else {
7379
- finalMessage =
7380
- loopResult.finalAssistantMessage || loopResult.fullMessage;
7381
- }
7356
+ finalMessage =
7357
+ loopResult.finalAssistantMessage || loopResult.fullMessage;
7382
7358
  lastContextWindow = loopResult.contextWindow;
7383
7359
  // 10. Notify callback
7384
7360
  options?.onTurnComplete?.(turnResult);
@@ -7463,8 +7439,7 @@ class Graphlit {
7463
7439
  }
7464
7440
  else {
7465
7441
  status = "error";
7466
- errorMessage =
7467
- error instanceof Error ? error.message : "Unknown error";
7442
+ errorMessage = error instanceof Error ? error.message : "Unknown error";
7468
7443
  }
7469
7444
  return this.buildRunAgentResult({
7470
7445
  agentId: resolvedAgentId,
@@ -7577,6 +7552,28 @@ class Graphlit {
7577
7552
  }
7578
7553
  return results;
7579
7554
  }
7555
+ /**
7556
+ * Detect whether one tool call is a task_complete invocation, either
7557
+ * directly or through a meta-executor such as execute_tool.
7558
+ */
7559
+ isTaskCompleteToolCall(toolCall) {
7560
+ if (!toolCall)
7561
+ return false;
7562
+ // Direct invocation
7563
+ if (toolCall.name === "task_complete")
7564
+ return true;
7565
+ // Meta-executor wrapping (e.g. execute_tool({ tool: "task_complete", parameters: {...} }))
7566
+ if (toolCall.arguments) {
7567
+ try {
7568
+ const args = JSON.parse(toolCall.arguments);
7569
+ return args.tool === "task_complete";
7570
+ }
7571
+ catch {
7572
+ // Not JSON — skip
7573
+ }
7574
+ }
7575
+ return false;
7576
+ }
7580
7577
  /**
7581
7578
  * Detect whether task_complete was invoked in a single list of tool calls.
7582
7579
  *
@@ -7597,22 +7594,8 @@ class Graphlit {
7597
7594
  if (!toolCalls)
7598
7595
  return false;
7599
7596
  for (const tc of toolCalls) {
7600
- if (!tc)
7601
- continue;
7602
- // Direct invocation
7603
- if (tc.name === "task_complete")
7597
+ if (this.isTaskCompleteToolCall(tc))
7604
7598
  return true;
7605
- // Meta-executor wrapping (e.g. execute_tool({ tool: "task_complete", parameters: {...} }))
7606
- if (tc.arguments) {
7607
- try {
7608
- const args = JSON.parse(tc.arguments);
7609
- if (args.tool === "task_complete")
7610
- return true;
7611
- }
7612
- catch {
7613
- // Not JSON — skip
7614
- }
7615
- }
7616
7599
  }
7617
7600
  return false;
7618
7601
  }
@@ -7628,107 +7611,6 @@ class Graphlit {
7628
7611
  }
7629
7612
  return false;
7630
7613
  }
7631
- /**
7632
- * Extract the `final_message` argument from a task_complete invocation
7633
- * within a list of tool calls. Handles both direct invocation and
7634
- * meta-executor wrapping (mirrors `detectTaskComplete`).
7635
- *
7636
- * Returns undefined if task_complete was not called, the arg is missing,
7637
- * or the arg is not a non-empty string.
7638
- */
7639
- extractTaskCompleteFinalMessage(toolCalls) {
7640
- if (!toolCalls)
7641
- return undefined;
7642
- for (const tc of toolCalls) {
7643
- if (!tc?.arguments)
7644
- continue;
7645
- let args;
7646
- try {
7647
- args = JSON.parse(tc.arguments);
7648
- }
7649
- catch {
7650
- continue;
7651
- }
7652
- // Direct invocation: args ARE the task_complete params
7653
- if (tc.name === "task_complete") {
7654
- const fm = args.final_message;
7655
- if (typeof fm === "string" && fm.trim().length > 0)
7656
- return fm;
7657
- continue;
7658
- }
7659
- // Meta-executor wrapping: { tool: "task_complete", parameters: {...} }
7660
- if (args.tool === "task_complete") {
7661
- const params = args.parameters;
7662
- const fm = params?.final_message;
7663
- if (typeof fm === "string" && fm.trim().length > 0)
7664
- return fm;
7665
- }
7666
- }
7667
- return undefined;
7668
- }
7669
- /**
7670
- * Scan a list of messages for a task_complete invocation's `final_message`
7671
- * arg. Convenience wrapper over `extractTaskCompleteFinalMessage`.
7672
- */
7673
- extractTaskCompleteFinalMessageFromMessages(messages) {
7674
- for (const msg of messages) {
7675
- const fm = this.extractTaskCompleteFinalMessage(msg.toolCalls);
7676
- if (fm)
7677
- return fm;
7678
- }
7679
- return undefined;
7680
- }
7681
- /**
7682
- * Sanitize task_complete's `final_message` payload out of persisted
7683
- * intermediate messages. The tool call record (name, id, timing, ordering)
7684
- * is preserved as a completion marker, but its `final_message` arg is
7685
- * stripped because that content is already stored as the turn's assistant
7686
- * text via `completion`. Keeping the tool call without the payload yields
7687
- * the same storage win as full removal while preserving the trace.
7688
- *
7689
- * Handles both direct and meta-executor (`execute_tool({tool: ...})`)
7690
- * invocations, matching `detectTaskComplete`.
7691
- */
7692
- sanitizeTaskCompletePayload(messages) {
7693
- return messages.map((msg) => {
7694
- if (!msg.toolCalls || msg.toolCalls.length === 0)
7695
- return msg;
7696
- let mutated = false;
7697
- const sanitizedToolCalls = msg.toolCalls.map((tc) => {
7698
- if (!tc || !tc.arguments)
7699
- return tc;
7700
- let args;
7701
- try {
7702
- args = JSON.parse(tc.arguments);
7703
- }
7704
- catch {
7705
- return tc;
7706
- }
7707
- // Direct invocation: args ARE the task_complete params
7708
- if (tc.name === "task_complete" && "final_message" in args) {
7709
- const { final_message: _dropped, ...rest } = args;
7710
- void _dropped;
7711
- mutated = true;
7712
- return { ...tc, arguments: JSON.stringify(rest) };
7713
- }
7714
- // Meta-executor wrapping: { tool: "task_complete", parameters: {...} }
7715
- if (args.tool === "task_complete") {
7716
- const params = args.parameters;
7717
- if (params && "final_message" in params) {
7718
- const { final_message: _dropped, ...restParams } = params;
7719
- void _dropped;
7720
- mutated = true;
7721
- return {
7722
- ...tc,
7723
- arguments: JSON.stringify({ ...args, parameters: restParams }),
7724
- };
7725
- }
7726
- }
7727
- return tc;
7728
- });
7729
- return mutated ? { ...msg, toolCalls: sanitizedToolCalls } : msg;
7730
- });
7731
- }
7732
7614
  /**
7733
7615
  * Run LLM-as-judge quality assessment on a completed agent run.
7734
7616
  */
@@ -8286,7 +8168,9 @@ class Graphlit {
8286
8168
  if (!handler) {
8287
8169
  throw new Error(`No handler found for tool: ${toolCall.name}`);
8288
8170
  }
8289
- parsedArguments = toolCall.arguments ? JSON.parse(toolCall.arguments) : {};
8171
+ parsedArguments = toolCall.arguments
8172
+ ? JSON.parse(toolCall.arguments)
8173
+ : {};
8290
8174
  // Add timeout for individual tool calls (30 seconds)
8291
8175
  const toolTimeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Tool execution timeout")), 30000));
8292
8176
  result = await Promise.race([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphlit-client",
3
- "version": "1.0.20260420001",
3
+ "version": "1.0.20260420002",
4
4
  "description": "Graphlit API Client for TypeScript",
5
5
  "type": "module",
6
6
  "main": "./dist/client.js",