@deepagents/context 0.38.0 → 0.38.1

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/index.js CHANGED
@@ -199,6 +199,12 @@ function assistant(message2) {
199
199
  }
200
200
  };
201
201
  }
202
+ function toMessageFragment(item) {
203
+ if (isFragment(item) && isMessageFragment(item)) {
204
+ return item;
205
+ }
206
+ return message(item);
207
+ }
202
208
  function message(content) {
203
209
  const message2 = typeof content === "string" ? {
204
210
  id: generateId(),
@@ -1869,7 +1875,6 @@ import {
1869
1875
  NoSuchToolError as NoSuchToolError2,
1870
1876
  ToolCallRepairError,
1871
1877
  createUIMessageStream as createUIMessageStream2,
1872
- generateId as generateId4,
1873
1878
  isToolUIPart
1874
1879
  } from "ai";
1875
1880
 
@@ -1877,7 +1882,10 @@ import {
1877
1882
  import { z as z2 } from "zod";
1878
1883
 
1879
1884
  // packages/context/src/lib/engine.ts
1880
- import { validateUIMessages } from "ai";
1885
+ import {
1886
+ generateId as generateId4,
1887
+ validateUIMessages
1888
+ } from "ai";
1881
1889
  import { mergeWith } from "lodash-es";
1882
1890
 
1883
1891
  // packages/context/src/lib/estimate.ts
@@ -2585,6 +2593,9 @@ function estimateMessageContent(data) {
2585
2593
  function isLanguageModelUsage(value) {
2586
2594
  return typeof value === "object" && value !== null && "totalTokens" in value;
2587
2595
  }
2596
+ function isEmptyAssistantPlaceholder(message2) {
2597
+ return message2.role === "assistant" && message2.parts.length === 0;
2598
+ }
2588
2599
  var ContextEngine = class _ContextEngine {
2589
2600
  /** Non-message fragments (role, hints, etc.) - not persisted in graph */
2590
2601
  #fragments = [];
@@ -2766,6 +2777,120 @@ var ContextEngine = class _ContextEngine {
2766
2777
  const { turn } = await this.#getChainContext();
2767
2778
  return turn;
2768
2779
  }
2780
+ async firstUserMessage() {
2781
+ await this.#ensureInitialized();
2782
+ if (this.#branch?.headMessageId) {
2783
+ const chain = await this.#store.getMessageChain(
2784
+ this.#branch.headMessageId
2785
+ );
2786
+ for (const msg of chain) {
2787
+ if (msg.name === "user") return msg.data;
2788
+ }
2789
+ }
2790
+ for (const fragment2 of this.#pendingMessages) {
2791
+ if (fragment2.name !== "user") continue;
2792
+ if (!fragment2.codec) {
2793
+ throw new Error(`Fragment "${fragment2.name}" is missing codec.`);
2794
+ }
2795
+ return fragment2.codec.encode();
2796
+ }
2797
+ return void 0;
2798
+ }
2799
+ /**
2800
+ * Return the head of the conversation — pending tail or persisted branch head.
2801
+ *
2802
+ * Includes empty assistant placeholders (use this for id-lookup, not for
2803
+ * building model prompts — see `getMessages()` for prompt-ready output).
2804
+ *
2805
+ * @throws if the pending tail is missing an id (programming error).
2806
+ */
2807
+ async headMessage() {
2808
+ await this.#ensureInitialized();
2809
+ if (this.#pendingMessages.length > 0) {
2810
+ const tail = this.#pendingMessages[this.#pendingMessages.length - 1];
2811
+ if (!tail.id) {
2812
+ throw new Error(
2813
+ `headMessage: pending fragment "${tail.name}" is missing id`
2814
+ );
2815
+ }
2816
+ return { id: tail.id, name: tail.name };
2817
+ }
2818
+ if (this.#branch?.headMessageId) {
2819
+ const msg = await this.#store.getMessage(this.#branch.headMessageId);
2820
+ if (msg) return { id: msg.id, name: msg.name };
2821
+ }
2822
+ return void 0;
2823
+ }
2824
+ /**
2825
+ * Return the model-ready conversation: persisted chain plus pending fragments,
2826
+ * with empty assistant placeholders filtered out.
2827
+ *
2828
+ * For id-lookup use `headMessage()` instead — that one keeps placeholders.
2829
+ */
2830
+ async getMessages() {
2831
+ await this.#ensureInitialized();
2832
+ const messages = [];
2833
+ if (this.#branch?.headMessageId) {
2834
+ const chain = await this.#store.getMessageChain(
2835
+ this.#branch.headMessageId
2836
+ );
2837
+ for (const msg of chain) {
2838
+ const data = msg.data;
2839
+ if (isEmptyAssistantPlaceholder(data)) continue;
2840
+ messages.push(data);
2841
+ }
2842
+ }
2843
+ for (const fragment2 of this.#pendingMessages) {
2844
+ if (!fragment2.codec) {
2845
+ throw new Error(`Fragment "${fragment2.name}" is missing codec.`);
2846
+ }
2847
+ const encoded = fragment2.codec.encode();
2848
+ if (isEmptyAssistantPlaceholder(encoded)) continue;
2849
+ messages.push(encoded);
2850
+ }
2851
+ return messages.length === 0 ? [] : validateUIMessages({ messages });
2852
+ }
2853
+ /**
2854
+ * Advance the conversation by one turn. Required setup before `chat()`.
2855
+ *
2856
+ * - User input → persists the message AND appends an empty assistant
2857
+ * placeholder reserving the id of the next streamed response.
2858
+ * - Assistant input (tool-resume / continuation) → persists in-place
2859
+ * (`branch: false`), reusing the input's id.
2860
+ *
2861
+ * Always leaves the chain head as an assistant fragment, satisfying chat()'s
2862
+ * precondition.
2863
+ *
2864
+ * @returns the assistant id that will receive the streamed response — useful
2865
+ * for telemetry, optimistic UI, or correlating logs before the stream starts.
2866
+ * @throws if assistant input is missing an id.
2867
+ *
2868
+ * @example
2869
+ * ```ts
2870
+ * const assistantId = await context.continue(user('hi'));
2871
+ * const stream = await chat(agent); // streams into assistantId
2872
+ * ```
2873
+ */
2874
+ async continue(input) {
2875
+ const fragment2 = toMessageFragment(input);
2876
+ const isAssistantUpdate = fragment2.name === "assistant";
2877
+ let assistantId;
2878
+ if (isAssistantUpdate) {
2879
+ if (!fragment2.id) {
2880
+ throw new Error("continue: assistant input is missing id");
2881
+ }
2882
+ assistantId = fragment2.id;
2883
+ this.set(fragment2);
2884
+ } else {
2885
+ assistantId = generateId4();
2886
+ this.set(
2887
+ fragment2,
2888
+ assistant({ id: assistantId, role: "assistant", parts: [] })
2889
+ );
2890
+ }
2891
+ await this.save({ branch: !isAssistantUpdate });
2892
+ return assistantId;
2893
+ }
2769
2894
  /**
2770
2895
  * Add fragments to the context.
2771
2896
  *
@@ -2812,25 +2937,8 @@ var ContextEngine = class _ContextEngine {
2812
2937
  async resolve(options) {
2813
2938
  await this.#ensureInitialized();
2814
2939
  const systemPrompt = options.renderer.render(this.#renderableFragments);
2815
- const messages = [];
2816
- if (this.#branch?.headMessageId) {
2817
- const chain = await this.#store.getMessageChain(
2818
- this.#branch.headMessageId
2819
- );
2820
- for (const msg of chain) {
2821
- messages.push(msg.data);
2822
- }
2823
- }
2824
- for (const fragment2 of this.#pendingMessages) {
2825
- if (!fragment2.codec) {
2826
- throw new Error(`Fragment "${fragment2.name}" is missing codec.`);
2827
- }
2828
- messages.push(fragment2.codec.encode());
2829
- }
2830
- return {
2831
- systemPrompt,
2832
- messages: messages.length === 0 ? [] : await validateUIMessages({ messages })
2833
- };
2940
+ const messages = await this.getMessages();
2941
+ return { systemPrompt, messages };
2834
2942
  }
2835
2943
  /**
2836
2944
  * Save pending messages to the graph.
@@ -3746,55 +3854,75 @@ Examples:
3746
3854
  - "hi" -> New Conversation
3747
3855
  - "debug my python code" -> Python Debugging`;
3748
3856
  var titleSchema = z2.object({ title: z2.string() });
3749
- function extractText(message2) {
3750
- const cleaned = stripReminders(message2);
3751
- const textPart = cleaned.parts.find((p) => p.type === "text");
3752
- return textPart && "text" in textPart ? textPart.text : "";
3753
- }
3754
- function truncateTitle(text) {
3755
- if (!text) return "New Chat";
3756
- return text.length > 100 ? text.slice(0, 100) + "..." : text;
3757
- }
3758
- function staticChatTitle(message2) {
3759
- return truncateTitle(extractText(message2));
3760
- }
3761
- async function generateChatTitle(options) {
3762
- const text = extractText(options.message);
3763
- const fallback = truncateTitle(text);
3764
- if (!text) return fallback;
3765
- const store2 = new InMemoryContextStore();
3766
- const context = new ContextEngine({
3767
- store: store2,
3768
- chatId: crypto.randomUUID(),
3769
- userId: "system"
3770
- });
3771
- context.set(role(TITLE_PROMPT), user(text));
3772
- try {
3857
+ var TitleGenerator = class {
3858
+ #context;
3859
+ constructor(options) {
3860
+ this.#context = options.context;
3861
+ }
3862
+ async ensure(options) {
3863
+ const msg = await this.#firstUntitledUser();
3864
+ if (!msg) return null;
3865
+ try {
3866
+ const title = await this.#generateTitle(msg, options);
3867
+ return this.#applyTitle(title, "llm");
3868
+ } catch (error) {
3869
+ console.warn(
3870
+ "TitleGenerator: LLM title generation failed, falling back to static.",
3871
+ error
3872
+ );
3873
+ return this.#applyTitle(this.#staticTitle(msg), "static");
3874
+ }
3875
+ }
3876
+ async ensureStatic() {
3877
+ const msg = await this.#firstUntitledUser();
3878
+ if (!msg) return null;
3879
+ return this.#applyTitle(this.#staticTitle(msg), "static");
3880
+ }
3881
+ #staticTitle(message2) {
3882
+ return this.#truncateTitle(this.#extractText(message2));
3883
+ }
3884
+ async #generateTitle(message2, options) {
3885
+ const text = this.#extractText(message2);
3886
+ if (!text) {
3887
+ throw new Error(
3888
+ "Cannot generate chat title: message has no text content."
3889
+ );
3890
+ }
3891
+ const store2 = new InMemoryContextStore();
3892
+ const context = new ContextEngine({
3893
+ store: store2,
3894
+ chatId: crypto.randomUUID(),
3895
+ userId: "system"
3896
+ });
3897
+ context.set(role(TITLE_PROMPT), user(text));
3773
3898
  const { title } = await structuredOutput({
3774
3899
  context,
3775
3900
  model: options.model,
3776
3901
  schema: titleSchema
3777
3902
  }).generate({}, { abortSignal: options.abortSignal });
3778
- return title || fallback;
3779
- } catch (error) {
3780
- console.warn("Title generation failed, using fallback:", error);
3781
- return fallback;
3903
+ if (!title) {
3904
+ throw new Error("Title generation returned an empty string.");
3905
+ }
3906
+ return title;
3782
3907
  }
3783
- }
3784
-
3785
- // packages/context/src/lib/chat.ts
3786
- function toMessageFragment(item) {
3787
- if (isFragment(item) && isMessageFragment(item)) {
3788
- return item;
3908
+ #extractText(message2) {
3909
+ return extractPlainText(stripReminders(message2));
3789
3910
  }
3790
- return message(item);
3791
- }
3792
- function chatMessageToUIMessage(item) {
3793
- if (isFragment(item) && isMessageFragment(item)) {
3794
- return item.codec.encode();
3911
+ #truncateTitle(text) {
3912
+ if (!text) return "New Chat";
3913
+ return text.length > 100 ? text.slice(0, 100) + "..." : text;
3795
3914
  }
3796
- return item;
3797
- }
3915
+ async #applyTitle(title, source) {
3916
+ await this.#context.updateChat({ title });
3917
+ return { title, source };
3918
+ }
3919
+ async #firstUntitledUser() {
3920
+ if (this.#context.chat?.title) return void 0;
3921
+ return this.#context.firstUserMessage();
3922
+ }
3923
+ };
3924
+
3925
+ // packages/context/src/lib/chat.ts
3798
3926
  var defaultChatMessageMetadata = ({
3799
3927
  part
3800
3928
  }) => {
@@ -3806,7 +3934,7 @@ var defaultChatMessageMetadata = ({
3806
3934
  }
3807
3935
  return void 0;
3808
3936
  };
3809
- async function chat(agent2, messages, options) {
3937
+ async function chat(agent2, options = {}) {
3810
3938
  const context = agent2.context;
3811
3939
  const sandbox = agent2.sandbox;
3812
3940
  if (!context) {
@@ -3814,53 +3942,36 @@ async function chat(agent2, messages, options) {
3814
3942
  "Agent is missing a context. Provide context when creating the agent."
3815
3943
  );
3816
3944
  }
3817
- if (messages.length === 0) {
3818
- throw new Error("messages must not be empty");
3819
- }
3820
- const lastItem = messages[messages.length - 1];
3821
- const lastFragment = toMessageFragment(lastItem);
3822
- const lastUIMessage = chatMessageToUIMessage(lastItem);
3823
- let assistantMsgId;
3824
- if (lastUIMessage.role === "assistant") {
3825
- context.set(lastFragment);
3826
- await context.save({ branch: false });
3827
- assistantMsgId = lastUIMessage.id;
3828
- } else {
3829
- context.set(lastFragment);
3830
- await context.save();
3831
- assistantMsgId = generateId4();
3832
- }
3833
- const uiMessages = messages.map(chatMessageToUIMessage);
3834
- let title = null;
3835
- if (!context.chat?.title) {
3836
- const firstUserMsg = uiMessages.find((m) => m.role === "user");
3837
- if (firstUserMsg) {
3838
- if (options?.generateTitle && agent2.model) {
3839
- title = await generateChatTitle({
3840
- message: firstUserMsg,
3841
- model: agent2.model,
3842
- abortSignal: options?.abortSignal
3843
- });
3844
- } else {
3845
- title = staticChatTitle(firstUserMsg);
3846
- }
3847
- await context.updateChat({ title });
3848
- }
3945
+ const head = await context.headMessage();
3946
+ if (head?.name !== "assistant") {
3947
+ throw new Error(
3948
+ "chat: expected an assistant message at head. Call context.continue(input) before chat()."
3949
+ );
3849
3950
  }
3850
- const streamContextVariables = options?.contextVariables === void 0 ? {} : options.contextVariables;
3851
- const result = await agent2.stream(streamContextVariables, {
3852
- transform: options?.transform,
3853
- abortSignal: options?.abortSignal
3854
- });
3951
+ const assistantMsgId = head.id;
3952
+ const uiMessages = await context.getMessages();
3953
+ const streamContextVariables = options.contextVariables === void 0 ? {} : options.contextVariables;
3954
+ const [title, result] = await Promise.all([
3955
+ makeTitle({
3956
+ context,
3957
+ model: agent2.model,
3958
+ generateTitle: options.generateTitle,
3959
+ abortSignal: options.abortSignal
3960
+ }),
3961
+ agent2.stream(streamContextVariables, {
3962
+ transform: options.transform,
3963
+ abortSignal: options.abortSignal
3964
+ })
3965
+ ]);
3855
3966
  const uiStream = result.toUIMessageStream({
3856
- onError: options?.onError ?? formatChatError,
3967
+ onError: options.onError ?? formatChatError,
3857
3968
  sendStart: true,
3858
3969
  sendFinish: true,
3859
3970
  sendReasoning: true,
3860
3971
  sendSources: true,
3861
3972
  originalMessages: uiMessages,
3862
3973
  generateMessageId: () => assistantMsgId,
3863
- messageMetadata: options?.messageMetadata ?? defaultChatMessageMetadata
3974
+ messageMetadata: options.messageMetadata ?? defaultChatMessageMetadata
3864
3975
  });
3865
3976
  return createUIMessageStream2({
3866
3977
  originalMessages: uiMessages,
@@ -3883,7 +3994,7 @@ async function chat(agent2, messages, options) {
3883
3994
  }
3884
3995
  const drained = sandbox.drainFileEvents?.() ?? [];
3885
3996
  const fileEvents = isAborted ? [] : drained;
3886
- const finalMetadata = await options?.finalAssistantMetadata?.(normalizedMessage);
3997
+ const finalMetadata = await options.finalAssistantMetadata?.(normalizedMessage);
3887
3998
  const mergedMetadata = {
3888
3999
  ...normalizedMessage.metadata ?? {},
3889
4000
  ...fileEvents.length > 0 ? { fileEvents } : {},
@@ -3920,9 +4031,7 @@ function sanitizeAbortedParts(parts) {
3920
4031
  sanitized.push(part);
3921
4032
  continue;
3922
4033
  }
3923
- if (part.state === "input-streaming") {
3924
- continue;
3925
- }
4034
+ if (part.state === "input-streaming") continue;
3926
4035
  sanitized.push({
3927
4036
  ...part,
3928
4037
  state: "output-error",
@@ -3947,6 +4056,19 @@ function formatChatError(error) {
3947
4056
  }
3948
4057
  return JSON.stringify(error);
3949
4058
  }
4059
+ async function makeTitle(options) {
4060
+ const titler = new TitleGenerator({ context: options.context });
4061
+ if (options.generateTitle && !options.model) {
4062
+ console.warn(
4063
+ "chat: generateTitle=true but agent.model is unset; using static title."
4064
+ );
4065
+ }
4066
+ const result = options.generateTitle && options.model ? await titler.ensure({
4067
+ model: options.model,
4068
+ abortSignal: options.abortSignal
4069
+ }) : await titler.ensureStatic();
4070
+ return result?.title ?? null;
4071
+ }
3950
4072
 
3951
4073
  // packages/context/src/lib/fragments/user.ts
3952
4074
  function identity(input) {
@@ -9211,6 +9333,7 @@ export {
9211
9333
  SqliteStreamStore,
9212
9334
  StreamManager,
9213
9335
  StreamStore,
9336
+ TitleGenerator,
9214
9337
  TomlRenderer,
9215
9338
  ToonRenderer,
9216
9339
  XmlRenderer,
@@ -9234,7 +9357,6 @@ export {
9234
9357
  buildSubcommandRepair,
9235
9358
  chainHooks,
9236
9359
  chat,
9237
- chatMessageToUIMessage,
9238
9360
  clarification,
9239
9361
  classifies,
9240
9362
  contentIncludes,
@@ -9272,7 +9394,6 @@ export {
9272
9394
  formatResponse,
9273
9395
  fragment,
9274
9396
  fromFragment,
9275
- generateChatTitle,
9276
9397
  getFragmentData,
9277
9398
  getLocaleFromMessage,
9278
9399
  getModelsRegistry,
@@ -9335,7 +9456,6 @@ export {
9335
9456
  skillsReminder,
9336
9457
  socraticPrompting,
9337
9458
  soul,
9338
- staticChatTitle,
9339
9459
  stop,
9340
9460
  stripQuoteArtifacts,
9341
9461
  stripReminders,