graphlit-client 1.0.20260419003 → 1.0.20260420001

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
@@ -3119,6 +3119,32 @@ declare class Graphlit {
3119
3119
  * with `intermediateMessages` rather than a single tool-call list.
3120
3120
  */
3121
3121
  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;
3122
3148
  /**
3123
3149
  * Run LLM-as-judge quality assessment on a completed agent run.
3124
3150
  */
package/dist/client.js CHANGED
@@ -6981,22 +6981,32 @@ class Graphlit {
6981
6981
  }
6982
6982
  // Build tools list with task_complete prepended.
6983
6983
  //
6984
- // task_complete is a bare completion signal — it takes no arguments.
6985
- // The agent's user-facing answer MUST be in its final assistant message
6986
- // (the `fullMessage` / `finalAssistantMessage` on the turn result); this
6987
- // tool only marks the run as done.
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.
6988
6988
  //
6989
- // Earlier versions had a `summary` field. That consistently caused the
6990
- // model to stuff its answer into the tool argument and then write a
6991
- // useless prose message ("answer delivered above") — the user would see
6992
- // the empty prose, not the answer. Removing the field eliminates the
6993
- // hiding place.
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.
6994
+ //
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.
6994
6999
  const taskCompleteTool = {
6995
7000
  name: "task_complete",
6996
- 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.",
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.",
6997
7002
  schema: JSON.stringify({
6998
7003
  type: "object",
6999
- properties: {},
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
+ },
7000
7010
  }),
7001
7011
  };
7002
7012
  const allTools = [
@@ -7254,9 +7264,39 @@ class Graphlit {
7254
7264
  finally {
7255
7265
  uiAdapter.dispose();
7256
7266
  }
7257
- // 8. Complete conversation (persist turn)
7258
- const trimmedMessage = loopResult.finalAssistantMessage;
7259
- if (trimmedMessage) {
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.
7271
+ 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
+ }
7298
+ // 8b. Complete conversation (persist turn)
7299
+ if (assistantMessageToPersist) {
7260
7300
  const completionTime = uiAdapter.getCompletionTime();
7261
7301
  const ttft = uiAdapter.getTTFT();
7262
7302
  const throughput = uiAdapter.getThroughput();
@@ -7265,13 +7305,20 @@ class Graphlit {
7265
7305
  return undefined;
7266
7306
  return `PT${ms / 1000}S`;
7267
7307
  };
7268
- let turnMessageInputs = loopResult.intermediateMessages.length > 0
7269
- ? loopResult.intermediateMessages
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
7270
7317
  : undefined;
7271
7318
  if (loopResult.lastRoundReasoning) {
7272
7319
  const completionInput = {
7273
7320
  role: Types.ConversationRoleTypes.Assistant,
7274
- message: trimmedMessage,
7321
+ message: assistantMessageToPersist,
7275
7322
  timestamp: new Date().toISOString(),
7276
7323
  thinkingContent: loopResult.lastRoundReasoning.content,
7277
7324
  };
@@ -7284,7 +7331,7 @@ class Graphlit {
7284
7331
  completionInput,
7285
7332
  ];
7286
7333
  }
7287
- await this.completeConversation(trimmedMessage, conversationId, millisecondsToTimeSpan(completionTime), millisecondsToTimeSpan(ttft), throughput, undefined, turnMessageInputs, options?.correlationId);
7334
+ await this.completeConversation(assistantMessageToPersist, conversationId, millisecondsToTimeSpan(completionTime), millisecondsToTimeSpan(ttft), throughput, undefined, turnMessageInputs, options?.correlationId);
7288
7335
  // Emit completion event
7289
7336
  uiAdapter.handleEvent({
7290
7337
  type: "complete",
@@ -7293,12 +7340,11 @@ class Graphlit {
7293
7340
  }
7294
7341
  // 9. Build TurnResult
7295
7342
  const turnDuration = Date.now() - turnStart;
7296
- const taskCompleteThisTurn = this.detectTaskCompleteInMessages(loopResult.intermediateMessages);
7297
7343
  const turnToolNames = [...new Set(loopResult.toolCallNames)].sort();
7298
7344
  const turnResult = {
7299
7345
  turnNumber: turn,
7300
7346
  prompt: currentPrompt,
7301
- responseText: loopResult.fullMessage,
7347
+ responseText: taskCompleteFinalMessage || loopResult.fullMessage,
7302
7348
  toolCalls: turnToolNames,
7303
7349
  toolCallCount: loopResult.toolCallCount,
7304
7350
  durationMs: turnDuration,
@@ -7314,7 +7360,25 @@ class Graphlit {
7314
7360
  lastTurnUsage = undefined;
7315
7361
  turnResults.push(turnResult);
7316
7362
  totalToolCalls += loopResult.toolCallCount;
7317
- finalMessage = loopResult.finalAssistantMessage || loopResult.fullMessage;
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
+ }
7318
7382
  lastContextWindow = loopResult.contextWindow;
7319
7383
  // 10. Notify callback
7320
7384
  options?.onTurnComplete?.(turnResult);
@@ -7564,6 +7628,107 @@ class Graphlit {
7564
7628
  }
7565
7629
  return false;
7566
7630
  }
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
+ }
7567
7732
  /**
7568
7733
  * Run LLM-as-judge quality assessment on a completed agent run.
7569
7734
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphlit-client",
3
- "version": "1.0.20260419003",
3
+ "version": "1.0.20260420001",
4
4
  "description": "Graphlit API Client for TypeScript",
5
5
  "type": "module",
6
6
  "main": "./dist/client.js",