@standardagents/builder 0.38.2 → 0.39.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
@@ -1498,6 +1498,9 @@ function normalizeToolCallMessageSequence(messages) {
1498
1498
  for (const message of messages) {
1499
1499
  const isAssistantToolCallMessage = message.role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
1500
1500
  if (!pending) {
1501
+ if (message.role === "tool") {
1502
+ continue;
1503
+ }
1501
1504
  if (!isAssistantToolCallMessage) {
1502
1505
  normalized.push(message);
1503
1506
  continue;
@@ -4283,6 +4286,24 @@ var init_ToolExecutor = __esm({
4283
4286
  }
4284
4287
  } catch {
4285
4288
  }
4289
+ const aliasTarget = this.subagentAliasTarget(state, toolName);
4290
+ if (aliasTarget) {
4291
+ if (packageId) {
4292
+ try {
4293
+ const qualified = aliasTarget.includes("/") ? aliasTarget : `${packageId}/${aliasTarget}`;
4294
+ if (await state.thread.instance.loadAgent(qualified)) {
4295
+ return "agent";
4296
+ }
4297
+ } catch {
4298
+ }
4299
+ }
4300
+ try {
4301
+ if (await state.thread.instance.loadAgent(aliasTarget)) {
4302
+ return "agent";
4303
+ }
4304
+ } catch {
4305
+ }
4306
+ }
4286
4307
  const isInToolsArray = (agentName) => {
4287
4308
  const tools2 = state.prompt._tools || [];
4288
4309
  return tools2.some((t) => typeof t === "string" ? t === agentName : t.name === agentName);
@@ -4798,15 +4819,20 @@ Message one with subagent_message(referenceId, message).`
4798
4819
  const builtinCreateAttachmentValue = createArgs?.attachments;
4799
4820
  const builtinCreateInitialArguments = createArgs?.initialArguments ?? {};
4800
4821
  const registryAgentName = targetAgentForCreate || call.function.name;
4822
+ const aliasConfig = this.resolveSubagentToolConfig(state, registryAgentName);
4823
+ const implementingAgentName = typeof aliasConfig.agent === "string" && aliasConfig.agent.trim().length > 0 ? aliasConfig.agent.trim() : registryAgentName;
4801
4824
  const packageId = getPackageId(state);
4802
4825
  let agentDef = null;
4803
- let agentName = registryAgentName;
4826
+ let agentName = implementingAgentName;
4804
4827
  if (builtinSubagentAction === "create" && targetAgentForCreate) {
4805
4828
  const availableSubagents = await this.getPromptSubagentNames(state);
4806
4829
  if (!availableSubagents.includes(targetAgentForCreate)) {
4830
+ const configuredDirect = (state.prompt._tools || []).some(
4831
+ (t) => typeof t === "string" ? t === targetAgentForCreate : t?.name === targetAgentForCreate
4832
+ );
4807
4833
  return {
4808
4834
  status: "error",
4809
- error: `Subagent "${targetAgentForCreate}" is not available for this prompt. Use one of: ${availableSubagents.join(", ") || "(none)"}.`
4835
+ error: configuredDirect ? `"${targetAgentForCreate}" is not a resumable subagent \u2014 call the ${targetAgentForCreate} tool directly; it runs to completion and returns its result in the tool response. subagent_create manages resumable subagents only: ${availableSubagents.join(", ") || "(none)"}.` : `Subagent "${targetAgentForCreate}" is not available for this prompt. Use one of: ${availableSubagents.join(", ") || "(none)"}.`
4810
4836
  };
4811
4837
  }
4812
4838
  }
@@ -4847,7 +4873,7 @@ Message one with subagent_message(referenceId, message).`
4847
4873
  if (agentType === "ai_human") {
4848
4874
  const parentToolConfigs = state.prompt._tools || [];
4849
4875
  const toolConfig = parentToolConfigs.find(
4850
- (t) => typeof t === "string" ? t === agentName : t.name === agentName
4876
+ (t) => typeof t === "string" ? t === registryAgentName || t === agentName : t.name === registryAgentName || t.name === agentName
4851
4877
  );
4852
4878
  const subpromptToolConfig = this.getSubpromptToolConfig(toolConfig);
4853
4879
  const targetState = state.rootState || state;
@@ -5425,6 +5451,27 @@ Message one with subagent_message(referenceId, message).`
5425
5451
  }
5426
5452
  return null;
5427
5453
  }
5454
+ /**
5455
+ * The implementing agent behind an aliased subagent tool name, or null when
5456
+ * the name carries no alias (`agent` absent, empty, or equal to the name).
5457
+ */
5458
+ static subagentAliasTarget(state, toolName) {
5459
+ for (const tool of state.prompt._tools || []) {
5460
+ if (!tool || typeof tool !== "object" || tool.name !== toolName) {
5461
+ continue;
5462
+ }
5463
+ const target = tool.agent;
5464
+ if (typeof target === "string" && target.trim().length > 0 && target.trim() !== toolName) {
5465
+ return target.trim();
5466
+ }
5467
+ }
5468
+ return null;
5469
+ }
5470
+ /**
5471
+ * Resolve the prompt's subagent tool config for an EXPOSED tool name.
5472
+ * Config entries are keyed by `name` (the model-facing tool name), which
5473
+ * may alias a differently-named implementing agent via `agent`.
5474
+ */
5428
5475
  static resolveSubagentToolConfig(state, agentName) {
5429
5476
  const config = (state.prompt._tools || []).find(
5430
5477
  (tool) => typeof tool === "string" ? tool === agentName : tool.name === agentName
@@ -7600,28 +7647,12 @@ This usually points to something I can't fix from here \u2014 for example the wo
7600
7647
  state.promptPath = [sideAPrompt.name];
7601
7648
  state.pendingImmediateAfterHandoff = true;
7602
7649
  if (userMessageContent || userMessageAttachments) {
7603
- const handoffMessageId = crypto.randomUUID();
7604
- const handoffTimestamp = Date.now() * 1e3;
7605
- const handoffMessage = {
7606
- id: handoffMessageId,
7607
- role: "user",
7608
- content: userMessageContent || "",
7609
- created_at: handoffTimestamp,
7610
- attachments: userMessageAttachments
7611
- };
7612
- await state.storage.sql.exec(
7613
- `INSERT INTO messages (id, role, content, attachments, created_at, prompt) VALUES (?, ?, ?, ?, ?, ?)`,
7614
- handoffMessage.id,
7615
- handoffMessage.role,
7616
- handoffMessage.content,
7617
- handoffMessage.attachments || null,
7618
- handoffMessage.created_at,
7650
+ await this.persistHandoffUserMessage(
7651
+ state,
7652
+ userMessageContent || "",
7653
+ userMessageAttachments,
7619
7654
  sideAPrompt.name
7620
7655
  );
7621
- state.messageHistory.push(handoffMessage);
7622
- if (state.emitMessage) {
7623
- state.emitMessage(handoffMessage);
7624
- }
7625
7656
  }
7626
7657
  state.pendingHandoff = void 0;
7627
7658
  state.emitTelemetry?.({
@@ -7636,6 +7667,37 @@ This usually points to something I can't fix from here \u2014 for example the wo
7636
7667
  throw error;
7637
7668
  }
7638
7669
  }
7670
+ /**
7671
+ * Persist the user message that rides an agent handoff, applying the
7672
+ * create-message lifecycle hooks like every other insertion path.
7673
+ */
7674
+ static async persistHandoffUserMessage(state, content, attachments, promptName) {
7675
+ let handoffMessage = {
7676
+ id: crypto.randomUUID(),
7677
+ role: "user",
7678
+ content,
7679
+ created_at: Date.now() * 1e3,
7680
+ status: "completed",
7681
+ attachments: attachments ?? null
7682
+ };
7683
+ handoffMessage = await this.runBeforeCreateMessageHook(state, handoffMessage);
7684
+ await state.storage.sql.exec(
7685
+ `INSERT INTO messages (id, role, content, attachments, created_at, status, silent, prompt) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
7686
+ handoffMessage.id,
7687
+ handoffMessage.role,
7688
+ handoffMessage.content,
7689
+ handoffMessage.attachments || null,
7690
+ handoffMessage.created_at,
7691
+ handoffMessage.status ?? "completed",
7692
+ handoffMessage.silent ? 1 : 0,
7693
+ promptName
7694
+ );
7695
+ state.messageHistory.push(handoffMessage);
7696
+ await this.runAfterCreateMessageHook(state, handoffMessage);
7697
+ if (state.emitMessage) {
7698
+ state.emitMessage(handoffMessage);
7699
+ }
7700
+ }
7639
7701
  static isEnabledEnvFlag(value) {
7640
7702
  if (value === null || value === void 0) return false;
7641
7703
  const normalized = value.trim().toLowerCase();
@@ -8533,7 +8595,16 @@ This usually points to something I can't fix from here \u2014 for example the wo
8533
8595
  role: "system",
8534
8596
  content: systemPromptText
8535
8597
  });
8536
- const completedMessages = state.messageHistory.filter(
8598
+ const historySnapshot = state.messageHistory.filter(
8599
+ (msg) => msg.status !== "pending"
8600
+ );
8601
+ const filteredHistory = await this.runFilterMessagesHook(
8602
+ state.thread.instance,
8603
+ state,
8604
+ historySnapshot
8605
+ );
8606
+ const contextHistory = this.filterOrphanedToolCalls(filteredHistory);
8607
+ const completedMessages = contextHistory.filter(
8537
8608
  (msg) => msg.status !== "pending"
8538
8609
  );
8539
8610
  const recentMessageThreshold = state.prompt.recentImageThreshold ?? 10;
@@ -8971,7 +9042,8 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
8971
9042
  }
8972
9043
  if (foundToolNames.has(toolName)) continue;
8973
9044
  try {
8974
- const agentDef = await state.thread.instance.loadAgent(toolName);
9045
+ const implementingAgent = subagentConfig && typeof subagentConfig.agent === "string" && subagentConfig.agent.trim().length > 0 ? subagentConfig.agent.trim() : toolName;
9046
+ const agentDef = await state.thread.instance.loadAgent(implementingAgent);
8975
9047
  if (agentDef && (agentDef.type === "ai_human" || agentDef.type === "dual_ai")) {
8976
9048
  let parameters;
8977
9049
  const sideAPromptName = agentDef.sideA?.prompt;
@@ -9185,12 +9257,14 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
9185
9257
  if (!toolName || toolName.startsWith("provider:") || seen.has(toolName)) {
9186
9258
  continue;
9187
9259
  }
9188
- const agentDef = await this.tryLoadPromptToolAgent(state, toolName, packageId);
9260
+ const subagentConfigForLoad = this.isSubagentToolConfig(configEntry) ? configEntry : void 0;
9261
+ const implementingAgent = subagentConfigForLoad && typeof subagentConfigForLoad.agent === "string" && subagentConfigForLoad.agent.trim().length > 0 ? subagentConfigForLoad.agent.trim() : toolName;
9262
+ const agentDef = await this.tryLoadPromptToolAgent(state, implementingAgent, packageId);
9189
9263
  if (!agentDef || agentDef.type !== "dual_ai") {
9190
9264
  continue;
9191
9265
  }
9192
9266
  seen.add(toolName);
9193
- const subagentConfig = this.isSubagentToolConfig(configEntry) ? configEntry : void 0;
9267
+ const subagentConfig = subagentConfigForLoad;
9194
9268
  const blocking = subagentConfig?.blocking ?? true;
9195
9269
  const resumable = !!subagentConfig?.resumable && typeof subagentConfig.resumable === "object";
9196
9270
  const receivesMessages = subagentConfig?.resumable && typeof subagentConfig.resumable === "object" ? subagentConfig.resumable.receives_messages : "side_a";
@@ -9668,11 +9742,11 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
9668
9742
  * @param message - Message object to be created
9669
9743
  * @returns Processed message (or original if hook doesn't exist/fails)
9670
9744
  */
9671
- static async runBeforeCreateMessageHook(state, message) {
9745
+ static async runBeforeCreateMessageHook(state, message, threadStateOverride, options) {
9672
9746
  const executors = await this.getHooksForType(state, "before_create_message");
9673
9747
  if (executors.length === 0) return message;
9674
9748
  let result = message;
9675
- const threadState = ThreadStateImpl.fromFlowState(state);
9749
+ const threadState = threadStateOverride ?? ThreadStateImpl.fromFlowState(state);
9676
9750
  for (const executor of executors) {
9677
9751
  try {
9678
9752
  const hooked = await executor(threadState, _FlowEngine.rowToHookMessage(result));
@@ -9682,6 +9756,9 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
9682
9756
  attachments: hooked.attachments === void 0 ? result.attachments : hooked.attachments === null ? null : typeof hooked.attachments === "string" ? hooked.attachments : JSON.stringify(hooked.attachments)
9683
9757
  };
9684
9758
  } catch (error) {
9759
+ if (options?.propagateHookErrors) {
9760
+ throw error;
9761
+ }
9685
9762
  console.error("[Hooks] \u2717 Error running before_create_message hook:", error);
9686
9763
  }
9687
9764
  }
@@ -9694,10 +9771,10 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
9694
9771
  * @param state - Flow state with execution context
9695
9772
  * @param message - The created message object
9696
9773
  */
9697
- static async runAfterCreateMessageHook(state, message) {
9774
+ static async runAfterCreateMessageHook(state, message, threadStateOverride) {
9698
9775
  const executors = await this.getHooksForType(state, "after_create_message");
9699
9776
  if (executors.length === 0) return;
9700
- const threadState = ThreadStateImpl.fromFlowState(state);
9777
+ const threadState = threadStateOverride ?? ThreadStateImpl.fromFlowState(state);
9701
9778
  for (const executor of executors) {
9702
9779
  try {
9703
9780
  await executor(threadState, _FlowEngine.rowToHookMessage(message));
@@ -10198,7 +10275,7 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
10198
10275
  }
10199
10276
  existingPathMessages.add(content);
10200
10277
  const attachmentsJson = JSON.stringify([ref]);
10201
- const silentMessage = {
10278
+ let silentMessage = {
10202
10279
  id: crypto.randomUUID(),
10203
10280
  role: sourceMessage.role,
10204
10281
  content,
@@ -10209,6 +10286,7 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
10209
10286
  parent_id: parentId,
10210
10287
  depth
10211
10288
  };
10289
+ silentMessage = await this.runBeforeCreateMessageHook(state, silentMessage);
10212
10290
  await state.storage.sql.exec(
10213
10291
  `
10214
10292
  INSERT INTO messages (
@@ -10219,15 +10297,16 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
10219
10297
  silentMessage.id,
10220
10298
  silentMessage.role,
10221
10299
  silentMessage.content,
10222
- attachmentsJson,
10223
- 1,
10300
+ silentMessage.attachments ?? null,
10301
+ silentMessage.silent ? 1 : 0,
10224
10302
  silentMessage.created_at,
10225
- silentMessage.status,
10303
+ silentMessage.status ?? "completed",
10226
10304
  silentMessage.parent_id,
10227
10305
  silentMessage.depth,
10228
10306
  currentPrompt
10229
10307
  );
10230
10308
  state.messageHistory.push(silentMessage);
10309
+ await this.runAfterCreateMessageHook(state, silentMessage);
10231
10310
  if (state.emitMessage) {
10232
10311
  state.emitMessage(silentMessage);
10233
10312
  }
@@ -10766,7 +10845,7 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
10766
10845
  */
10767
10846
  static async loadMessageHistory(state) {
10768
10847
  const storage = state.storage;
10769
- const thread = state.thread;
10848
+ state.thread;
10770
10849
  const includeParentHistory = state.prompt.include_chat ?? true;
10771
10850
  const includeToolCalls = state.prompt.include_past_tools ?? true;
10772
10851
  try {
@@ -10802,8 +10881,11 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
10802
10881
  rows = cursor.toArray();
10803
10882
  }
10804
10883
  if (state.extraMessages && state.extraMessages.length > 0) {
10805
- rows.push(...state.extraMessages);
10806
- rows.sort((a, b) => a.created_at - b.created_at || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
10884
+ const persistedIds = new Set(rows.map((row) => row.id));
10885
+ rows.push(
10886
+ ...state.extraMessages.filter((entry) => !persistedIds.has(entry.id))
10887
+ );
10888
+ rows.sort((a, b) => a.created_at - b.created_at);
10807
10889
  }
10808
10890
  const messages = rows.map((row) => ({
10809
10891
  id: row.id,
@@ -10826,24 +10908,15 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
10826
10908
  reasoning_content: row.reasoning_content,
10827
10909
  reasoning_details: row.reasoning_details
10828
10910
  }));
10829
- return this.runFilterMessagesHook(
10830
- thread.instance,
10831
- state,
10832
- this.filterOrphanedToolCalls(messages)
10833
- );
10911
+ return messages;
10834
10912
  } catch (error) {
10835
10913
  console.error("Error loading message history:", error);
10836
10914
  return [];
10837
10915
  }
10838
10916
  }
10839
10917
  /**
10840
- * Run the filter_messages hook if it exists
10841
- * This hook allows developers to modify SQL row data before transformation
10842
- *
10843
- * @param thread - Thread instance with hooks registry
10844
- * @param state - Flow state for hook context
10845
- * @param rows - SQL rows from messages table
10846
- * @returns Processed rows (or original if hook doesn't exist/fails)
10918
+ * Build request-context rows from persisted history.
10919
+ * Mutations and synthetic messages stay in the request context.
10847
10920
  */
10848
10921
  static async runFilterMessagesHook(thread, state, messages) {
10849
10922
  const executors = await this.getHooksForType(state, "filter_messages");
@@ -10856,13 +10929,56 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
10856
10929
  for (const row of result) if (row.id) byId.set(row.id, row);
10857
10930
  const hookView = result.map((row) => _FlowEngine.rowToHookMessage(row));
10858
10931
  const kept = await executor(threadState, hookView);
10859
- result = kept.map((m) => m.id && byId.get(m.id) || null).filter((row) => row !== null);
10932
+ result = kept.map((hookedMessage) => {
10933
+ const original = hookedMessage.id ? byId.get(hookedMessage.id) : void 0;
10934
+ if (original) {
10935
+ return _FlowEngine.applyHookMessageToRow(original, hookedMessage);
10936
+ }
10937
+ return _FlowEngine.hookMessageToContextRow(hookedMessage);
10938
+ });
10860
10939
  } catch (error) {
10861
10940
  console.error("[Hooks] \u2717 Error running filter_messages hook:", error);
10862
10941
  }
10863
10942
  }
10864
10943
  return result;
10865
10944
  }
10945
+ /**
10946
+ * Copy a persisted row and apply hook edits. The original live-history
10947
+ * object is left unchanged.
10948
+ */
10949
+ static applyHookMessageToRow(original, hooked) {
10950
+ const attachments = hooked.attachments === void 0 ? original.attachments : hooked.attachments === null ? null : typeof hooked.attachments === "string" ? hooked.attachments : JSON.stringify(hooked.attachments);
10951
+ return {
10952
+ ...original,
10953
+ role: hooked.role ?? original.role,
10954
+ content: hooked.content !== void 0 ? hooked.content : original.content,
10955
+ name: hooked.name !== void 0 ? hooked.name : original.name,
10956
+ tool_calls: hooked.tool_calls !== void 0 ? hooked.tool_calls : original.tool_calls,
10957
+ tool_call_id: hooked.tool_call_id !== void 0 ? hooked.tool_call_id : original.tool_call_id,
10958
+ silent: hooked.silent !== void 0 ? hooked.silent : original.silent,
10959
+ attachments
10960
+ };
10961
+ }
10962
+ /**
10963
+ * Convert a synthetic hook message into a request-context-only builder row.
10964
+ */
10965
+ static hookMessageToContextRow(hooked) {
10966
+ const attachments = hooked.attachments === void 0 || hooked.attachments === null ? hooked.attachments ?? null : typeof hooked.attachments === "string" ? hooked.attachments : JSON.stringify(hooked.attachments);
10967
+ return {
10968
+ id: hooked.id || crypto.randomUUID(),
10969
+ role: hooked.role,
10970
+ content: hooked.content ?? null,
10971
+ name: hooked.name ?? null,
10972
+ tool_calls: typeof hooked.tool_calls === "string" ? hooked.tool_calls : null,
10973
+ tool_call_id: hooked.tool_call_id ?? null,
10974
+ created_at: hooked.created_at ?? Date.now() * TIMESTAMP_MULTIPLIER,
10975
+ parent_id: hooked.parent_id ?? null,
10976
+ depth: hooked.depth ?? 0,
10977
+ silent: hooked.silent ?? false,
10978
+ status: "completed",
10979
+ attachments
10980
+ };
10981
+ }
10866
10982
  /**
10867
10983
  * Log an error to the logs table (best-effort)
10868
10984
  * Creates a log entry with error details but no associated message_id
@@ -11006,6 +11122,35 @@ ${errorStack}` : ""}`;
11006
11122
  });
11007
11123
 
11008
11124
  // src/agents/utilities.ts
11125
+ var utilities_exports = {};
11126
+ __export(utilities_exports, {
11127
+ cat: () => cat,
11128
+ emitThreadEvent: () => emitThreadEvent,
11129
+ exists: () => exists,
11130
+ find: () => find,
11131
+ forceTurn: () => forceTurn,
11132
+ getFileStats: () => getFileStats,
11133
+ getMessages: () => getMessages,
11134
+ getThumbnail: () => getThumbnail,
11135
+ grep: () => grep,
11136
+ head: () => head,
11137
+ injectMessage: () => injectMessage,
11138
+ linkFile: () => linkFile,
11139
+ mkdir: () => mkdir,
11140
+ move: () => move,
11141
+ queueTool: () => queueTool,
11142
+ readFile: () => readFile,
11143
+ readdir: () => readdir,
11144
+ reloadHistory: () => reloadHistory,
11145
+ rename: () => rename,
11146
+ rmdir: () => rmdir,
11147
+ stat: () => stat,
11148
+ tail: () => tail,
11149
+ unlink: () => unlink,
11150
+ updateThread: () => updateThread,
11151
+ writeFile: () => writeFile,
11152
+ writeImage: () => writeImage
11153
+ });
11009
11154
  function queueTool(flow, toolName, args = {}) {
11010
11155
  if (flow.currentToolUses !== void 0) {
11011
11156
  if (!flow.currentToolUses.includes(toolName)) {
@@ -11033,7 +11178,7 @@ async function injectMessage(flow, options) {
11033
11178
  const timestamp = Date.now() * TIMESTAMP_MULTIPLIER;
11034
11179
  const silent = options.silent ?? false;
11035
11180
  const forceTopLevel = options.forceTopLevel ?? false;
11036
- const message = {
11181
+ let message = {
11037
11182
  id: messageId,
11038
11183
  role: options.role,
11039
11184
  content: options.content,
@@ -11047,41 +11192,52 @@ async function injectMessage(flow, options) {
11047
11192
  metadata: options.metadata
11048
11193
  };
11049
11194
  if (options.beforeMessageId) {
11050
- const cursor = await flow.storage.sql.exec(
11051
- `SELECT created_at FROM messages WHERE id = ?`,
11052
- options.beforeMessageId
11195
+ const liveTarget = flow.messageHistory.find(
11196
+ (entry) => entry.id === options.beforeMessageId
11053
11197
  );
11054
- const rows = cursor.toArray();
11055
- const targetMessage = rows[0];
11056
- if (!targetMessage) {
11057
- throw new Error(
11058
- `Message with id ${options.beforeMessageId} not found`
11198
+ let targetCreatedAt = liveTarget?.created_at;
11199
+ if (targetCreatedAt === void 0) {
11200
+ const cursor = await flow.storage.sql.exec(
11201
+ `SELECT created_at FROM messages WHERE id = ?`,
11202
+ options.beforeMessageId
11059
11203
  );
11204
+ const rows = cursor.toArray();
11205
+ const targetMessage = rows[0];
11206
+ if (!targetMessage) {
11207
+ throw new Error(
11208
+ `Message with id ${options.beforeMessageId} not found`
11209
+ );
11210
+ }
11211
+ targetCreatedAt = targetMessage.created_at;
11060
11212
  }
11061
- message.created_at = targetMessage.created_at - 1;
11213
+ message.created_at = targetCreatedAt - 1;
11062
11214
  }
11063
11215
  const currentPrompt = flow.promptPath.length > 0 ? flow.promptPath[flow.promptPath.length - 1] : null;
11064
- const metadataJson = options.metadata ? JSON.stringify(options.metadata) : null;
11216
+ const { FlowEngine: FlowEngine2 } = await Promise.resolve().then(() => (init_FlowEngine(), FlowEngine_exports));
11217
+ message = await FlowEngine2.runBeforeCreateMessageHook(flow, message, void 0, {
11218
+ propagateHookErrors: true
11219
+ });
11220
+ const metadataJson = message.metadata === void 0 ? null : JSON.stringify(message.metadata);
11065
11221
  await flow.storage.sql.exec(
11066
11222
  `INSERT INTO messages (id, role, content, name, tool_call_id, created_at, status, silent, parent_id, depth, metadata, prompt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
11067
- messageId,
11223
+ message.id,
11068
11224
  message.role,
11069
11225
  message.content,
11070
11226
  message.name,
11071
11227
  message.tool_call_id,
11072
11228
  message.created_at,
11073
11229
  message.status,
11074
- silent ? 1 : 0,
11230
+ message.silent ? 1 : 0,
11075
11231
  message.parent_id,
11076
11232
  message.depth,
11077
11233
  metadataJson,
11078
11234
  currentPrompt
11079
11235
  );
11080
- if (!silent) {
11081
- flow.messageHistory.push(message);
11082
- } else {
11083
- flow.messageHistory = await reloadHistory(flow);
11236
+ appendLiveExecutionHistory(flow, message, options.beforeMessageId);
11237
+ if (flow.prompt.include_chat === false || message.parent_id !== null || (message.depth ?? 0) > 0) {
11238
+ appendInjectedContextMessage(flow.extraMessages, message);
11084
11239
  }
11240
+ await FlowEngine2.runAfterCreateMessageHook(flow, message);
11085
11241
  if (flow.emitMessage) {
11086
11242
  flow.emitMessage(message);
11087
11243
  }
@@ -11090,6 +11246,42 @@ async function injectMessage(flow, options) {
11090
11246
  }
11091
11247
  return message;
11092
11248
  }
11249
+ function appendLiveExecutionHistory(flow, message, beforeMessageId) {
11250
+ const history = flow.messageHistory;
11251
+ const existing = history.findIndex((entry) => entry.id === message.id);
11252
+ if (existing >= 0) {
11253
+ history.splice(existing, 1);
11254
+ }
11255
+ if (beforeMessageId) {
11256
+ const targetIndex = history.findIndex((entry) => entry.id === beforeMessageId);
11257
+ if (targetIndex >= 0) {
11258
+ history.splice(targetIndex, 0, message);
11259
+ return;
11260
+ }
11261
+ }
11262
+ const insertAt = history.findIndex(
11263
+ (entry) => entry.created_at > message.created_at
11264
+ );
11265
+ if (insertAt === -1) {
11266
+ history.push(message);
11267
+ return;
11268
+ }
11269
+ history.splice(insertAt, 0, message);
11270
+ }
11271
+ function appendInjectedContextMessage(extraMessages, message) {
11272
+ const existing = extraMessages.findIndex((entry) => entry.id === message.id);
11273
+ if (existing >= 0) {
11274
+ extraMessages.splice(existing, 1);
11275
+ }
11276
+ const insertAt = extraMessages.findIndex(
11277
+ (entry) => entry.created_at > message.created_at
11278
+ );
11279
+ if (insertAt === -1) {
11280
+ extraMessages.push(message);
11281
+ return;
11282
+ }
11283
+ extraMessages.splice(insertAt, 0, message);
11284
+ }
11093
11285
  async function getMessages(flow, limit = 100, offset = 0, order = "asc") {
11094
11286
  const orderClause = order === "desc" ? "DESC" : "ASC";
11095
11287
  const cursor = await flow.storage.sql.exec(
@@ -29983,6 +30175,7 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
29983
30175
  // src/durable-objects/DurableThread.ts
29984
30176
  init_StreamManager();
29985
30177
  init_LLMRequest();
30178
+ init_ThreadStateImpl();
29986
30179
 
29987
30180
  // src/durable-objects/logDetailStream.ts
29988
30181
  var LOG_DETAIL_HEAVY_FIELDS = [
@@ -30685,7 +30878,8 @@ var DurableThread = class _DurableThread extends DurableObject {
30685
30878
  side_b_session_fail_attachments_property: sideBSession.fail.attachmentsProperty,
30686
30879
  side_b_session_status_tool: sideBSession.status.toolName,
30687
30880
  side_b_session_status_message_property: sideBSession.status.messageProperty,
30688
- side_b_session_status_attachments_property: sideBSession.status.attachmentsProperty
30881
+ side_b_session_status_attachments_property: sideBSession.status.attachmentsProperty,
30882
+ hooks: Array.isArray(agentDef.hooks) ? agentDef.hooks : void 0
30689
30883
  };
30690
30884
  }
30691
30885
  /**
@@ -31355,6 +31549,29 @@ var DurableThread = class _DurableThread extends DurableObject {
31355
31549
  }
31356
31550
  return projections;
31357
31551
  }
31552
+ /**
31553
+ * Build hook resolution context and a genuine at-rest ThreadState.
31554
+ * Resolution failures abort insertion so lifecycle hooks cannot be bypassed.
31555
+ */
31556
+ async buildAtRestHookState(threadId) {
31557
+ try {
31558
+ const metadata = await this.getThreadMetadata(threadId);
31559
+ const agentConfig = await this.loadAgentConfig(metadata.agent_id);
31560
+ const threadInstance = this;
31561
+ return {
31562
+ hookState: {
31563
+ agentConfig,
31564
+ thread: { instance: threadInstance, metadata }
31565
+ },
31566
+ threadState: ThreadStateImpl.fromThreadInstance(threadInstance, metadata)
31567
+ };
31568
+ } catch (error) {
31569
+ throw new Error(
31570
+ `Unable to resolve create-message hooks for ${threadId}`,
31571
+ { cause: error }
31572
+ );
31573
+ }
31574
+ }
31358
31575
  /**
31359
31576
  * Inject a top-level message without starting a new execution.
31360
31577
  *
@@ -31367,22 +31584,7 @@ var DurableThread = class _DurableThread extends DurableObject {
31367
31584
  const id = crypto.randomUUID();
31368
31585
  const createdAt = Date.now() * TIMESTAMP_MULTIPLIER;
31369
31586
  const silent = message.silent === true;
31370
- const metadataJson = message.metadata ? JSON.stringify(message.metadata) : null;
31371
- await this.ctx.storage.sql.exec(
31372
- `INSERT INTO messages (id, role, content, created_at, status, silent, parent_id, depth, metadata, prompt)
31373
- VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)`,
31374
- id,
31375
- message.role,
31376
- message.content,
31377
- createdAt,
31378
- "completed",
31379
- silent ? 1 : 0,
31380
- null,
31381
- 0,
31382
- metadataJson,
31383
- null
31384
- );
31385
- const inserted = {
31587
+ let inserted = {
31386
31588
  id,
31387
31589
  role: message.role,
31388
31590
  content: message.content,
@@ -31393,9 +31595,37 @@ var DurableThread = class _DurableThread extends DurableObject {
31393
31595
  depth: 0,
31394
31596
  metadata: message.metadata
31395
31597
  };
31598
+ const { hookState, threadState } = await this.buildAtRestHookState(threadId);
31599
+ const { FlowEngine: FlowEngine2 } = await Promise.resolve().then(() => (init_FlowEngine(), FlowEngine_exports));
31600
+ inserted = await FlowEngine2.runBeforeCreateMessageHook(
31601
+ hookState,
31602
+ inserted,
31603
+ threadState,
31604
+ { propagateHookErrors: true }
31605
+ );
31606
+ const metadataJson = inserted.metadata ? JSON.stringify(inserted.metadata) : null;
31607
+ await this.ctx.storage.sql.exec(
31608
+ `INSERT INTO messages (id, role, content, created_at, status, silent, parent_id, depth, metadata, prompt)
31609
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)`,
31610
+ inserted.id,
31611
+ inserted.role,
31612
+ inserted.content,
31613
+ inserted.created_at,
31614
+ inserted.status ?? "completed",
31615
+ inserted.silent ? 1 : 0,
31616
+ inserted.parent_id ?? null,
31617
+ inserted.depth ?? 0,
31618
+ metadataJson,
31619
+ null
31620
+ );
31621
+ await FlowEngine2.runAfterCreateMessageHook(
31622
+ hookState,
31623
+ inserted,
31624
+ threadState
31625
+ );
31396
31626
  this.broadcastMessage(inserted);
31397
31627
  const record = inserted;
31398
- const visibleUserInjection = message.role === "user" && !silent && typeof message.content === "string" && message.content.trim().length > 0 && !isRuntimeSubagentEnvelope(message.content);
31628
+ const visibleUserInjection = inserted.role === "user" && !inserted.silent && typeof inserted.content === "string" && inserted.content.trim().length > 0 && !isRuntimeSubagentEnvelope(inserted.content);
31399
31629
  if (visibleUserInjection || isVisibleTerminalAssistantMessage(record)) {
31400
31630
  const stamp = this.recordRegistryActivity(
31401
31631
  threadId,
@@ -37890,8 +38120,7 @@ var DurableUserHub = class extends DurableObject {
37890
38120
  init_utilities();
37891
38121
 
37892
38122
  // src/agents/FlowStateSdk.ts
37893
- init_types();
37894
- var FlowStateSdk = class _FlowStateSdk {
38123
+ var FlowStateSdk = class {
37895
38124
  /**
37896
38125
  * Queue a new tool call to be executed in the current flow
37897
38126
  *
@@ -37958,63 +38187,8 @@ var FlowStateSdk = class _FlowStateSdk {
37958
38187
  * ```
37959
38188
  */
37960
38189
  static async injectMessage(flow, options) {
37961
- const messageId = options.id ?? crypto.randomUUID();
37962
- const timestamp = Date.now() * TIMESTAMP_MULTIPLIER;
37963
- const silent = options.silent ?? false;
37964
- const forceTopLevel = options.forceTopLevel ?? false;
37965
- const message = {
37966
- id: messageId,
37967
- role: options.role,
37968
- content: options.content,
37969
- name: options.name ?? null,
37970
- tool_call_id: options.toolCallId ?? null,
37971
- created_at: timestamp,
37972
- status: "completed",
37973
- silent,
37974
- parent_id: forceTopLevel ? null : flow.parentMessageId ?? null,
37975
- depth: forceTopLevel ? 0 : flow.depth
37976
- };
37977
- if (options.beforeMessageId) {
37978
- const cursor = await flow.storage.sql.exec(
37979
- `SELECT created_at FROM messages WHERE id = ?`,
37980
- options.beforeMessageId
37981
- );
37982
- const rows = cursor.toArray();
37983
- const targetMessage = rows[0];
37984
- if (!targetMessage) {
37985
- throw new Error(
37986
- `Message with id ${options.beforeMessageId} not found`
37987
- );
37988
- }
37989
- message.created_at = targetMessage.created_at - 1;
37990
- }
37991
- const currentPrompt = flow.promptPath.length > 0 ? flow.promptPath[flow.promptPath.length - 1] : null;
37992
- await flow.storage.sql.exec(
37993
- `INSERT INTO messages (id, role, content, name, tool_call_id, created_at, status, silent, parent_id, depth, prompt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
37994
- messageId,
37995
- message.role,
37996
- message.content,
37997
- message.name,
37998
- message.tool_call_id,
37999
- message.created_at,
38000
- message.status,
38001
- silent ? 1 : 0,
38002
- message.parent_id,
38003
- message.depth,
38004
- currentPrompt
38005
- );
38006
- if (!silent) {
38007
- flow.messageHistory.push(message);
38008
- } else {
38009
- flow.messageHistory = await _FlowStateSdk.reloadHistory(flow);
38010
- }
38011
- if (flow.emitMessage) {
38012
- flow.emitMessage(message);
38013
- }
38014
- if (options.forceTurn) {
38015
- _FlowStateSdk.forceTurn(flow, options.forceTurn);
38016
- }
38017
- return message;
38190
+ const { injectMessage: injectMessage2 } = await Promise.resolve().then(() => (init_utilities(), utilities_exports));
38191
+ return injectMessage2(flow, options);
38018
38192
  }
38019
38193
  /**
38020
38194
  * Get messages from the thread history
@@ -38103,9 +38277,9 @@ var FlowStateSdk = class _FlowStateSdk {
38103
38277
  /**
38104
38278
  * Reload message history from storage
38105
38279
  *
38106
- * Re-loads the complete message history from the thread's SQLite storage,
38107
- * applying any filter_messages hooks. This is useful when you need to refresh
38108
- * the message history after making changes (e.g., injecting silent messages).
38280
+ * Re-loads the complete message history from the thread's SQLite storage.
38281
+ * This is useful when you need to refresh the message history after making
38282
+ * changes (e.g., injecting messages).
38109
38283
  *
38110
38284
  * @param flow - The current FlowState
38111
38285
  * @returns Array of messages (reloaded from storage)