@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/runtime.js CHANGED
@@ -1489,6 +1489,9 @@ function normalizeToolCallMessageSequence(messages) {
1489
1489
  for (const message of messages) {
1490
1490
  const isAssistantToolCallMessage = message.role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
1491
1491
  if (!pending) {
1492
+ if (message.role === "tool") {
1493
+ continue;
1494
+ }
1492
1495
  if (!isAssistantToolCallMessage) {
1493
1496
  normalized.push(message);
1494
1497
  continue;
@@ -4274,6 +4277,24 @@ var init_ToolExecutor = __esm({
4274
4277
  }
4275
4278
  } catch {
4276
4279
  }
4280
+ const aliasTarget = this.subagentAliasTarget(state, toolName);
4281
+ if (aliasTarget) {
4282
+ if (packageId) {
4283
+ try {
4284
+ const qualified = aliasTarget.includes("/") ? aliasTarget : `${packageId}/${aliasTarget}`;
4285
+ if (await state.thread.instance.loadAgent(qualified)) {
4286
+ return "agent";
4287
+ }
4288
+ } catch {
4289
+ }
4290
+ }
4291
+ try {
4292
+ if (await state.thread.instance.loadAgent(aliasTarget)) {
4293
+ return "agent";
4294
+ }
4295
+ } catch {
4296
+ }
4297
+ }
4277
4298
  const isInToolsArray = (agentName) => {
4278
4299
  const tools2 = state.prompt._tools || [];
4279
4300
  return tools2.some((t) => typeof t === "string" ? t === agentName : t.name === agentName);
@@ -4789,15 +4810,20 @@ Message one with subagent_message(referenceId, message).`
4789
4810
  const builtinCreateAttachmentValue = createArgs?.attachments;
4790
4811
  const builtinCreateInitialArguments = createArgs?.initialArguments ?? {};
4791
4812
  const registryAgentName = targetAgentForCreate || call.function.name;
4813
+ const aliasConfig = this.resolveSubagentToolConfig(state, registryAgentName);
4814
+ const implementingAgentName = typeof aliasConfig.agent === "string" && aliasConfig.agent.trim().length > 0 ? aliasConfig.agent.trim() : registryAgentName;
4792
4815
  const packageId = getPackageId(state);
4793
4816
  let agentDef = null;
4794
- let agentName = registryAgentName;
4817
+ let agentName = implementingAgentName;
4795
4818
  if (builtinSubagentAction === "create" && targetAgentForCreate) {
4796
4819
  const availableSubagents = await this.getPromptSubagentNames(state);
4797
4820
  if (!availableSubagents.includes(targetAgentForCreate)) {
4821
+ const configuredDirect = (state.prompt._tools || []).some(
4822
+ (t) => typeof t === "string" ? t === targetAgentForCreate : t?.name === targetAgentForCreate
4823
+ );
4798
4824
  return {
4799
4825
  status: "error",
4800
- error: `Subagent "${targetAgentForCreate}" is not available for this prompt. Use one of: ${availableSubagents.join(", ") || "(none)"}.`
4826
+ 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)"}.`
4801
4827
  };
4802
4828
  }
4803
4829
  }
@@ -4838,7 +4864,7 @@ Message one with subagent_message(referenceId, message).`
4838
4864
  if (agentType === "ai_human") {
4839
4865
  const parentToolConfigs = state.prompt._tools || [];
4840
4866
  const toolConfig = parentToolConfigs.find(
4841
- (t) => typeof t === "string" ? t === agentName : t.name === agentName
4867
+ (t) => typeof t === "string" ? t === registryAgentName || t === agentName : t.name === registryAgentName || t.name === agentName
4842
4868
  );
4843
4869
  const subpromptToolConfig = this.getSubpromptToolConfig(toolConfig);
4844
4870
  const targetState = state.rootState || state;
@@ -5416,6 +5442,27 @@ Message one with subagent_message(referenceId, message).`
5416
5442
  }
5417
5443
  return null;
5418
5444
  }
5445
+ /**
5446
+ * The implementing agent behind an aliased subagent tool name, or null when
5447
+ * the name carries no alias (`agent` absent, empty, or equal to the name).
5448
+ */
5449
+ static subagentAliasTarget(state, toolName) {
5450
+ for (const tool of state.prompt._tools || []) {
5451
+ if (!tool || typeof tool !== "object" || tool.name !== toolName) {
5452
+ continue;
5453
+ }
5454
+ const target = tool.agent;
5455
+ if (typeof target === "string" && target.trim().length > 0 && target.trim() !== toolName) {
5456
+ return target.trim();
5457
+ }
5458
+ }
5459
+ return null;
5460
+ }
5461
+ /**
5462
+ * Resolve the prompt's subagent tool config for an EXPOSED tool name.
5463
+ * Config entries are keyed by `name` (the model-facing tool name), which
5464
+ * may alias a differently-named implementing agent via `agent`.
5465
+ */
5419
5466
  static resolveSubagentToolConfig(state, agentName) {
5420
5467
  const config = (state.prompt._tools || []).find(
5421
5468
  (tool) => typeof tool === "string" ? tool === agentName : tool.name === agentName
@@ -7591,28 +7638,12 @@ This usually points to something I can't fix from here \u2014 for example the wo
7591
7638
  state.promptPath = [sideAPrompt.name];
7592
7639
  state.pendingImmediateAfterHandoff = true;
7593
7640
  if (userMessageContent || userMessageAttachments) {
7594
- const handoffMessageId = crypto.randomUUID();
7595
- const handoffTimestamp = Date.now() * 1e3;
7596
- const handoffMessage = {
7597
- id: handoffMessageId,
7598
- role: "user",
7599
- content: userMessageContent || "",
7600
- created_at: handoffTimestamp,
7601
- attachments: userMessageAttachments
7602
- };
7603
- await state.storage.sql.exec(
7604
- `INSERT INTO messages (id, role, content, attachments, created_at, prompt) VALUES (?, ?, ?, ?, ?, ?)`,
7605
- handoffMessage.id,
7606
- handoffMessage.role,
7607
- handoffMessage.content,
7608
- handoffMessage.attachments || null,
7609
- handoffMessage.created_at,
7641
+ await this.persistHandoffUserMessage(
7642
+ state,
7643
+ userMessageContent || "",
7644
+ userMessageAttachments,
7610
7645
  sideAPrompt.name
7611
7646
  );
7612
- state.messageHistory.push(handoffMessage);
7613
- if (state.emitMessage) {
7614
- state.emitMessage(handoffMessage);
7615
- }
7616
7647
  }
7617
7648
  state.pendingHandoff = void 0;
7618
7649
  state.emitTelemetry?.({
@@ -7627,6 +7658,37 @@ This usually points to something I can't fix from here \u2014 for example the wo
7627
7658
  throw error;
7628
7659
  }
7629
7660
  }
7661
+ /**
7662
+ * Persist the user message that rides an agent handoff, applying the
7663
+ * create-message lifecycle hooks like every other insertion path.
7664
+ */
7665
+ static async persistHandoffUserMessage(state, content, attachments, promptName) {
7666
+ let handoffMessage = {
7667
+ id: crypto.randomUUID(),
7668
+ role: "user",
7669
+ content,
7670
+ created_at: Date.now() * 1e3,
7671
+ status: "completed",
7672
+ attachments: attachments ?? null
7673
+ };
7674
+ handoffMessage = await this.runBeforeCreateMessageHook(state, handoffMessage);
7675
+ await state.storage.sql.exec(
7676
+ `INSERT INTO messages (id, role, content, attachments, created_at, status, silent, prompt) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
7677
+ handoffMessage.id,
7678
+ handoffMessage.role,
7679
+ handoffMessage.content,
7680
+ handoffMessage.attachments || null,
7681
+ handoffMessage.created_at,
7682
+ handoffMessage.status ?? "completed",
7683
+ handoffMessage.silent ? 1 : 0,
7684
+ promptName
7685
+ );
7686
+ state.messageHistory.push(handoffMessage);
7687
+ await this.runAfterCreateMessageHook(state, handoffMessage);
7688
+ if (state.emitMessage) {
7689
+ state.emitMessage(handoffMessage);
7690
+ }
7691
+ }
7630
7692
  static isEnabledEnvFlag(value) {
7631
7693
  if (value === null || value === void 0) return false;
7632
7694
  const normalized = value.trim().toLowerCase();
@@ -8524,7 +8586,16 @@ This usually points to something I can't fix from here \u2014 for example the wo
8524
8586
  role: "system",
8525
8587
  content: systemPromptText
8526
8588
  });
8527
- const completedMessages = state.messageHistory.filter(
8589
+ const historySnapshot = state.messageHistory.filter(
8590
+ (msg) => msg.status !== "pending"
8591
+ );
8592
+ const filteredHistory = await this.runFilterMessagesHook(
8593
+ state.thread.instance,
8594
+ state,
8595
+ historySnapshot
8596
+ );
8597
+ const contextHistory = this.filterOrphanedToolCalls(filteredHistory);
8598
+ const completedMessages = contextHistory.filter(
8528
8599
  (msg) => msg.status !== "pending"
8529
8600
  );
8530
8601
  const recentMessageThreshold = state.prompt.recentImageThreshold ?? 10;
@@ -8962,7 +9033,8 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
8962
9033
  }
8963
9034
  if (foundToolNames.has(toolName)) continue;
8964
9035
  try {
8965
- const agentDef = await state.thread.instance.loadAgent(toolName);
9036
+ const implementingAgent = subagentConfig && typeof subagentConfig.agent === "string" && subagentConfig.agent.trim().length > 0 ? subagentConfig.agent.trim() : toolName;
9037
+ const agentDef = await state.thread.instance.loadAgent(implementingAgent);
8966
9038
  if (agentDef && (agentDef.type === "ai_human" || agentDef.type === "dual_ai")) {
8967
9039
  let parameters;
8968
9040
  const sideAPromptName = agentDef.sideA?.prompt;
@@ -9176,12 +9248,14 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
9176
9248
  if (!toolName || toolName.startsWith("provider:") || seen.has(toolName)) {
9177
9249
  continue;
9178
9250
  }
9179
- const agentDef = await this.tryLoadPromptToolAgent(state, toolName, packageId);
9251
+ const subagentConfigForLoad = this.isSubagentToolConfig(configEntry) ? configEntry : void 0;
9252
+ const implementingAgent = subagentConfigForLoad && typeof subagentConfigForLoad.agent === "string" && subagentConfigForLoad.agent.trim().length > 0 ? subagentConfigForLoad.agent.trim() : toolName;
9253
+ const agentDef = await this.tryLoadPromptToolAgent(state, implementingAgent, packageId);
9180
9254
  if (!agentDef || agentDef.type !== "dual_ai") {
9181
9255
  continue;
9182
9256
  }
9183
9257
  seen.add(toolName);
9184
- const subagentConfig = this.isSubagentToolConfig(configEntry) ? configEntry : void 0;
9258
+ const subagentConfig = subagentConfigForLoad;
9185
9259
  const blocking = subagentConfig?.blocking ?? true;
9186
9260
  const resumable = !!subagentConfig?.resumable && typeof subagentConfig.resumable === "object";
9187
9261
  const receivesMessages = subagentConfig?.resumable && typeof subagentConfig.resumable === "object" ? subagentConfig.resumable.receives_messages : "side_a";
@@ -9659,11 +9733,11 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
9659
9733
  * @param message - Message object to be created
9660
9734
  * @returns Processed message (or original if hook doesn't exist/fails)
9661
9735
  */
9662
- static async runBeforeCreateMessageHook(state, message) {
9736
+ static async runBeforeCreateMessageHook(state, message, threadStateOverride, options) {
9663
9737
  const executors = await this.getHooksForType(state, "before_create_message");
9664
9738
  if (executors.length === 0) return message;
9665
9739
  let result = message;
9666
- const threadState = ThreadStateImpl.fromFlowState(state);
9740
+ const threadState = threadStateOverride ?? ThreadStateImpl.fromFlowState(state);
9667
9741
  for (const executor of executors) {
9668
9742
  try {
9669
9743
  const hooked = await executor(threadState, _FlowEngine.rowToHookMessage(result));
@@ -9673,6 +9747,9 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
9673
9747
  attachments: hooked.attachments === void 0 ? result.attachments : hooked.attachments === null ? null : typeof hooked.attachments === "string" ? hooked.attachments : JSON.stringify(hooked.attachments)
9674
9748
  };
9675
9749
  } catch (error) {
9750
+ if (options?.propagateHookErrors) {
9751
+ throw error;
9752
+ }
9676
9753
  console.error("[Hooks] \u2717 Error running before_create_message hook:", error);
9677
9754
  }
9678
9755
  }
@@ -9685,10 +9762,10 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
9685
9762
  * @param state - Flow state with execution context
9686
9763
  * @param message - The created message object
9687
9764
  */
9688
- static async runAfterCreateMessageHook(state, message) {
9765
+ static async runAfterCreateMessageHook(state, message, threadStateOverride) {
9689
9766
  const executors = await this.getHooksForType(state, "after_create_message");
9690
9767
  if (executors.length === 0) return;
9691
- const threadState = ThreadStateImpl.fromFlowState(state);
9768
+ const threadState = threadStateOverride ?? ThreadStateImpl.fromFlowState(state);
9692
9769
  for (const executor of executors) {
9693
9770
  try {
9694
9771
  await executor(threadState, _FlowEngine.rowToHookMessage(message));
@@ -10189,7 +10266,7 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
10189
10266
  }
10190
10267
  existingPathMessages.add(content);
10191
10268
  const attachmentsJson = JSON.stringify([ref]);
10192
- const silentMessage = {
10269
+ let silentMessage = {
10193
10270
  id: crypto.randomUUID(),
10194
10271
  role: sourceMessage.role,
10195
10272
  content,
@@ -10200,6 +10277,7 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
10200
10277
  parent_id: parentId,
10201
10278
  depth
10202
10279
  };
10280
+ silentMessage = await this.runBeforeCreateMessageHook(state, silentMessage);
10203
10281
  await state.storage.sql.exec(
10204
10282
  `
10205
10283
  INSERT INTO messages (
@@ -10210,15 +10288,16 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
10210
10288
  silentMessage.id,
10211
10289
  silentMessage.role,
10212
10290
  silentMessage.content,
10213
- attachmentsJson,
10214
- 1,
10291
+ silentMessage.attachments ?? null,
10292
+ silentMessage.silent ? 1 : 0,
10215
10293
  silentMessage.created_at,
10216
- silentMessage.status,
10294
+ silentMessage.status ?? "completed",
10217
10295
  silentMessage.parent_id,
10218
10296
  silentMessage.depth,
10219
10297
  currentPrompt
10220
10298
  );
10221
10299
  state.messageHistory.push(silentMessage);
10300
+ await this.runAfterCreateMessageHook(state, silentMessage);
10222
10301
  if (state.emitMessage) {
10223
10302
  state.emitMessage(silentMessage);
10224
10303
  }
@@ -10757,7 +10836,7 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
10757
10836
  */
10758
10837
  static async loadMessageHistory(state) {
10759
10838
  const storage = state.storage;
10760
- const thread = state.thread;
10839
+ state.thread;
10761
10840
  const includeParentHistory = state.prompt.include_chat ?? true;
10762
10841
  const includeToolCalls = state.prompt.include_past_tools ?? true;
10763
10842
  try {
@@ -10793,8 +10872,11 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
10793
10872
  rows = cursor.toArray();
10794
10873
  }
10795
10874
  if (state.extraMessages && state.extraMessages.length > 0) {
10796
- rows.push(...state.extraMessages);
10797
- rows.sort((a, b) => a.created_at - b.created_at || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
10875
+ const persistedIds = new Set(rows.map((row) => row.id));
10876
+ rows.push(
10877
+ ...state.extraMessages.filter((entry) => !persistedIds.has(entry.id))
10878
+ );
10879
+ rows.sort((a, b) => a.created_at - b.created_at);
10798
10880
  }
10799
10881
  const messages = rows.map((row) => ({
10800
10882
  id: row.id,
@@ -10817,24 +10899,15 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
10817
10899
  reasoning_content: row.reasoning_content,
10818
10900
  reasoning_details: row.reasoning_details
10819
10901
  }));
10820
- return this.runFilterMessagesHook(
10821
- thread.instance,
10822
- state,
10823
- this.filterOrphanedToolCalls(messages)
10824
- );
10902
+ return messages;
10825
10903
  } catch (error) {
10826
10904
  console.error("Error loading message history:", error);
10827
10905
  return [];
10828
10906
  }
10829
10907
  }
10830
10908
  /**
10831
- * Run the filter_messages hook if it exists
10832
- * This hook allows developers to modify SQL row data before transformation
10833
- *
10834
- * @param thread - Thread instance with hooks registry
10835
- * @param state - Flow state for hook context
10836
- * @param rows - SQL rows from messages table
10837
- * @returns Processed rows (or original if hook doesn't exist/fails)
10909
+ * Build request-context rows from persisted history.
10910
+ * Mutations and synthetic messages stay in the request context.
10838
10911
  */
10839
10912
  static async runFilterMessagesHook(thread, state, messages) {
10840
10913
  const executors = await this.getHooksForType(state, "filter_messages");
@@ -10847,13 +10920,56 @@ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
10847
10920
  for (const row of result) if (row.id) byId.set(row.id, row);
10848
10921
  const hookView = result.map((row) => _FlowEngine.rowToHookMessage(row));
10849
10922
  const kept = await executor(threadState, hookView);
10850
- result = kept.map((m) => m.id && byId.get(m.id) || null).filter((row) => row !== null);
10923
+ result = kept.map((hookedMessage) => {
10924
+ const original = hookedMessage.id ? byId.get(hookedMessage.id) : void 0;
10925
+ if (original) {
10926
+ return _FlowEngine.applyHookMessageToRow(original, hookedMessage);
10927
+ }
10928
+ return _FlowEngine.hookMessageToContextRow(hookedMessage);
10929
+ });
10851
10930
  } catch (error) {
10852
10931
  console.error("[Hooks] \u2717 Error running filter_messages hook:", error);
10853
10932
  }
10854
10933
  }
10855
10934
  return result;
10856
10935
  }
10936
+ /**
10937
+ * Copy a persisted row and apply hook edits. The original live-history
10938
+ * object is left unchanged.
10939
+ */
10940
+ static applyHookMessageToRow(original, hooked) {
10941
+ const attachments = hooked.attachments === void 0 ? original.attachments : hooked.attachments === null ? null : typeof hooked.attachments === "string" ? hooked.attachments : JSON.stringify(hooked.attachments);
10942
+ return {
10943
+ ...original,
10944
+ role: hooked.role ?? original.role,
10945
+ content: hooked.content !== void 0 ? hooked.content : original.content,
10946
+ name: hooked.name !== void 0 ? hooked.name : original.name,
10947
+ tool_calls: hooked.tool_calls !== void 0 ? hooked.tool_calls : original.tool_calls,
10948
+ tool_call_id: hooked.tool_call_id !== void 0 ? hooked.tool_call_id : original.tool_call_id,
10949
+ silent: hooked.silent !== void 0 ? hooked.silent : original.silent,
10950
+ attachments
10951
+ };
10952
+ }
10953
+ /**
10954
+ * Convert a synthetic hook message into a request-context-only builder row.
10955
+ */
10956
+ static hookMessageToContextRow(hooked) {
10957
+ const attachments = hooked.attachments === void 0 || hooked.attachments === null ? hooked.attachments ?? null : typeof hooked.attachments === "string" ? hooked.attachments : JSON.stringify(hooked.attachments);
10958
+ return {
10959
+ id: hooked.id || crypto.randomUUID(),
10960
+ role: hooked.role,
10961
+ content: hooked.content ?? null,
10962
+ name: hooked.name ?? null,
10963
+ tool_calls: typeof hooked.tool_calls === "string" ? hooked.tool_calls : null,
10964
+ tool_call_id: hooked.tool_call_id ?? null,
10965
+ created_at: hooked.created_at ?? Date.now() * TIMESTAMP_MULTIPLIER,
10966
+ parent_id: hooked.parent_id ?? null,
10967
+ depth: hooked.depth ?? 0,
10968
+ silent: hooked.silent ?? false,
10969
+ status: "completed",
10970
+ attachments
10971
+ };
10972
+ }
10857
10973
  /**
10858
10974
  * Log an error to the logs table (best-effort)
10859
10975
  * Creates a log entry with error details but no associated message_id
@@ -10997,6 +11113,35 @@ ${errorStack}` : ""}`;
10997
11113
  });
10998
11114
 
10999
11115
  // src/agents/utilities.ts
11116
+ var utilities_exports = {};
11117
+ __export(utilities_exports, {
11118
+ cat: () => cat,
11119
+ emitThreadEvent: () => emitThreadEvent,
11120
+ exists: () => exists,
11121
+ find: () => find,
11122
+ forceTurn: () => forceTurn,
11123
+ getFileStats: () => getFileStats,
11124
+ getMessages: () => getMessages,
11125
+ getThumbnail: () => getThumbnail,
11126
+ grep: () => grep,
11127
+ head: () => head,
11128
+ injectMessage: () => injectMessage,
11129
+ linkFile: () => linkFile,
11130
+ mkdir: () => mkdir,
11131
+ move: () => move,
11132
+ queueTool: () => queueTool,
11133
+ readFile: () => readFile,
11134
+ readdir: () => readdir,
11135
+ reloadHistory: () => reloadHistory,
11136
+ rename: () => rename,
11137
+ rmdir: () => rmdir,
11138
+ stat: () => stat,
11139
+ tail: () => tail,
11140
+ unlink: () => unlink,
11141
+ updateThread: () => updateThread,
11142
+ writeFile: () => writeFile,
11143
+ writeImage: () => writeImage
11144
+ });
11000
11145
  function queueTool(flow, toolName, args = {}) {
11001
11146
  if (flow.currentToolUses !== void 0) {
11002
11147
  if (!flow.currentToolUses.includes(toolName)) {
@@ -11024,7 +11169,7 @@ async function injectMessage(flow, options) {
11024
11169
  const timestamp = Date.now() * TIMESTAMP_MULTIPLIER;
11025
11170
  const silent = options.silent ?? false;
11026
11171
  const forceTopLevel = options.forceTopLevel ?? false;
11027
- const message = {
11172
+ let message = {
11028
11173
  id: messageId,
11029
11174
  role: options.role,
11030
11175
  content: options.content,
@@ -11038,41 +11183,52 @@ async function injectMessage(flow, options) {
11038
11183
  metadata: options.metadata
11039
11184
  };
11040
11185
  if (options.beforeMessageId) {
11041
- const cursor = await flow.storage.sql.exec(
11042
- `SELECT created_at FROM messages WHERE id = ?`,
11043
- options.beforeMessageId
11186
+ const liveTarget = flow.messageHistory.find(
11187
+ (entry) => entry.id === options.beforeMessageId
11044
11188
  );
11045
- const rows = cursor.toArray();
11046
- const targetMessage = rows[0];
11047
- if (!targetMessage) {
11048
- throw new Error(
11049
- `Message with id ${options.beforeMessageId} not found`
11189
+ let targetCreatedAt = liveTarget?.created_at;
11190
+ if (targetCreatedAt === void 0) {
11191
+ const cursor = await flow.storage.sql.exec(
11192
+ `SELECT created_at FROM messages WHERE id = ?`,
11193
+ options.beforeMessageId
11050
11194
  );
11195
+ const rows = cursor.toArray();
11196
+ const targetMessage = rows[0];
11197
+ if (!targetMessage) {
11198
+ throw new Error(
11199
+ `Message with id ${options.beforeMessageId} not found`
11200
+ );
11201
+ }
11202
+ targetCreatedAt = targetMessage.created_at;
11051
11203
  }
11052
- message.created_at = targetMessage.created_at - 1;
11204
+ message.created_at = targetCreatedAt - 1;
11053
11205
  }
11054
11206
  const currentPrompt = flow.promptPath.length > 0 ? flow.promptPath[flow.promptPath.length - 1] : null;
11055
- const metadataJson = options.metadata ? JSON.stringify(options.metadata) : null;
11207
+ const { FlowEngine: FlowEngine2 } = await Promise.resolve().then(() => (init_FlowEngine(), FlowEngine_exports));
11208
+ message = await FlowEngine2.runBeforeCreateMessageHook(flow, message, void 0, {
11209
+ propagateHookErrors: true
11210
+ });
11211
+ const metadataJson = message.metadata === void 0 ? null : JSON.stringify(message.metadata);
11056
11212
  await flow.storage.sql.exec(
11057
11213
  `INSERT INTO messages (id, role, content, name, tool_call_id, created_at, status, silent, parent_id, depth, metadata, prompt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
11058
- messageId,
11214
+ message.id,
11059
11215
  message.role,
11060
11216
  message.content,
11061
11217
  message.name,
11062
11218
  message.tool_call_id,
11063
11219
  message.created_at,
11064
11220
  message.status,
11065
- silent ? 1 : 0,
11221
+ message.silent ? 1 : 0,
11066
11222
  message.parent_id,
11067
11223
  message.depth,
11068
11224
  metadataJson,
11069
11225
  currentPrompt
11070
11226
  );
11071
- if (!silent) {
11072
- flow.messageHistory.push(message);
11073
- } else {
11074
- flow.messageHistory = await reloadHistory(flow);
11227
+ appendLiveExecutionHistory(flow, message, options.beforeMessageId);
11228
+ if (flow.prompt.include_chat === false || message.parent_id !== null || (message.depth ?? 0) > 0) {
11229
+ appendInjectedContextMessage(flow.extraMessages, message);
11075
11230
  }
11231
+ await FlowEngine2.runAfterCreateMessageHook(flow, message);
11076
11232
  if (flow.emitMessage) {
11077
11233
  flow.emitMessage(message);
11078
11234
  }
@@ -11081,6 +11237,42 @@ async function injectMessage(flow, options) {
11081
11237
  }
11082
11238
  return message;
11083
11239
  }
11240
+ function appendLiveExecutionHistory(flow, message, beforeMessageId) {
11241
+ const history = flow.messageHistory;
11242
+ const existing = history.findIndex((entry) => entry.id === message.id);
11243
+ if (existing >= 0) {
11244
+ history.splice(existing, 1);
11245
+ }
11246
+ if (beforeMessageId) {
11247
+ const targetIndex = history.findIndex((entry) => entry.id === beforeMessageId);
11248
+ if (targetIndex >= 0) {
11249
+ history.splice(targetIndex, 0, message);
11250
+ return;
11251
+ }
11252
+ }
11253
+ const insertAt = history.findIndex(
11254
+ (entry) => entry.created_at > message.created_at
11255
+ );
11256
+ if (insertAt === -1) {
11257
+ history.push(message);
11258
+ return;
11259
+ }
11260
+ history.splice(insertAt, 0, message);
11261
+ }
11262
+ function appendInjectedContextMessage(extraMessages, message) {
11263
+ const existing = extraMessages.findIndex((entry) => entry.id === message.id);
11264
+ if (existing >= 0) {
11265
+ extraMessages.splice(existing, 1);
11266
+ }
11267
+ const insertAt = extraMessages.findIndex(
11268
+ (entry) => entry.created_at > message.created_at
11269
+ );
11270
+ if (insertAt === -1) {
11271
+ extraMessages.push(message);
11272
+ return;
11273
+ }
11274
+ extraMessages.splice(insertAt, 0, message);
11275
+ }
11084
11276
  async function getMessages(flow, limit = 100, offset = 0, order = "asc") {
11085
11277
  const orderClause = order === "desc" ? "DESC" : "ASC";
11086
11278
  const cursor = await flow.storage.sql.exec(
@@ -21508,6 +21700,7 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
21508
21700
  // src/durable-objects/DurableThread.ts
21509
21701
  init_StreamManager();
21510
21702
  init_LLMRequest();
21703
+ init_ThreadStateImpl();
21511
21704
 
21512
21705
  // src/durable-objects/logDetailStream.ts
21513
21706
  var LOG_DETAIL_HEAVY_FIELDS = [
@@ -22210,7 +22403,8 @@ var DurableThread = class _DurableThread extends DurableObject {
22210
22403
  side_b_session_fail_attachments_property: sideBSession.fail.attachmentsProperty,
22211
22404
  side_b_session_status_tool: sideBSession.status.toolName,
22212
22405
  side_b_session_status_message_property: sideBSession.status.messageProperty,
22213
- side_b_session_status_attachments_property: sideBSession.status.attachmentsProperty
22406
+ side_b_session_status_attachments_property: sideBSession.status.attachmentsProperty,
22407
+ hooks: Array.isArray(agentDef.hooks) ? agentDef.hooks : void 0
22214
22408
  };
22215
22409
  }
22216
22410
  /**
@@ -22880,6 +23074,29 @@ var DurableThread = class _DurableThread extends DurableObject {
22880
23074
  }
22881
23075
  return projections;
22882
23076
  }
23077
+ /**
23078
+ * Build hook resolution context and a genuine at-rest ThreadState.
23079
+ * Resolution failures abort insertion so lifecycle hooks cannot be bypassed.
23080
+ */
23081
+ async buildAtRestHookState(threadId) {
23082
+ try {
23083
+ const metadata = await this.getThreadMetadata(threadId);
23084
+ const agentConfig = await this.loadAgentConfig(metadata.agent_id);
23085
+ const threadInstance = this;
23086
+ return {
23087
+ hookState: {
23088
+ agentConfig,
23089
+ thread: { instance: threadInstance, metadata }
23090
+ },
23091
+ threadState: ThreadStateImpl.fromThreadInstance(threadInstance, metadata)
23092
+ };
23093
+ } catch (error) {
23094
+ throw new Error(
23095
+ `Unable to resolve create-message hooks for ${threadId}`,
23096
+ { cause: error }
23097
+ );
23098
+ }
23099
+ }
22883
23100
  /**
22884
23101
  * Inject a top-level message without starting a new execution.
22885
23102
  *
@@ -22892,22 +23109,7 @@ var DurableThread = class _DurableThread extends DurableObject {
22892
23109
  const id = crypto.randomUUID();
22893
23110
  const createdAt = Date.now() * TIMESTAMP_MULTIPLIER;
22894
23111
  const silent = message.silent === true;
22895
- const metadataJson = message.metadata ? JSON.stringify(message.metadata) : null;
22896
- await this.ctx.storage.sql.exec(
22897
- `INSERT INTO messages (id, role, content, created_at, status, silent, parent_id, depth, metadata, prompt)
22898
- VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)`,
22899
- id,
22900
- message.role,
22901
- message.content,
22902
- createdAt,
22903
- "completed",
22904
- silent ? 1 : 0,
22905
- null,
22906
- 0,
22907
- metadataJson,
22908
- null
22909
- );
22910
- const inserted = {
23112
+ let inserted = {
22911
23113
  id,
22912
23114
  role: message.role,
22913
23115
  content: message.content,
@@ -22918,9 +23120,37 @@ var DurableThread = class _DurableThread extends DurableObject {
22918
23120
  depth: 0,
22919
23121
  metadata: message.metadata
22920
23122
  };
23123
+ const { hookState, threadState } = await this.buildAtRestHookState(threadId);
23124
+ const { FlowEngine: FlowEngine2 } = await Promise.resolve().then(() => (init_FlowEngine(), FlowEngine_exports));
23125
+ inserted = await FlowEngine2.runBeforeCreateMessageHook(
23126
+ hookState,
23127
+ inserted,
23128
+ threadState,
23129
+ { propagateHookErrors: true }
23130
+ );
23131
+ const metadataJson = inserted.metadata ? JSON.stringify(inserted.metadata) : null;
23132
+ await this.ctx.storage.sql.exec(
23133
+ `INSERT INTO messages (id, role, content, created_at, status, silent, parent_id, depth, metadata, prompt)
23134
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)`,
23135
+ inserted.id,
23136
+ inserted.role,
23137
+ inserted.content,
23138
+ inserted.created_at,
23139
+ inserted.status ?? "completed",
23140
+ inserted.silent ? 1 : 0,
23141
+ inserted.parent_id ?? null,
23142
+ inserted.depth ?? 0,
23143
+ metadataJson,
23144
+ null
23145
+ );
23146
+ await FlowEngine2.runAfterCreateMessageHook(
23147
+ hookState,
23148
+ inserted,
23149
+ threadState
23150
+ );
22921
23151
  this.broadcastMessage(inserted);
22922
23152
  const record = inserted;
22923
- const visibleUserInjection = message.role === "user" && !silent && typeof message.content === "string" && message.content.trim().length > 0 && !isRuntimeSubagentEnvelope(message.content);
23153
+ const visibleUserInjection = inserted.role === "user" && !inserted.silent && typeof inserted.content === "string" && inserted.content.trim().length > 0 && !isRuntimeSubagentEnvelope(inserted.content);
22924
23154
  if (visibleUserInjection || isVisibleTerminalAssistantMessage(record)) {
22925
23155
  const stamp = this.recordRegistryActivity(
22926
23156
  threadId,
@@ -29415,8 +29645,7 @@ var DurableUserHub = class extends DurableObject {
29415
29645
  init_utilities();
29416
29646
 
29417
29647
  // src/agents/FlowStateSdk.ts
29418
- init_types();
29419
- var FlowStateSdk = class _FlowStateSdk {
29648
+ var FlowStateSdk = class {
29420
29649
  /**
29421
29650
  * Queue a new tool call to be executed in the current flow
29422
29651
  *
@@ -29483,63 +29712,8 @@ var FlowStateSdk = class _FlowStateSdk {
29483
29712
  * ```
29484
29713
  */
29485
29714
  static async injectMessage(flow, options) {
29486
- const messageId = options.id ?? crypto.randomUUID();
29487
- const timestamp = Date.now() * TIMESTAMP_MULTIPLIER;
29488
- const silent = options.silent ?? false;
29489
- const forceTopLevel = options.forceTopLevel ?? false;
29490
- const message = {
29491
- id: messageId,
29492
- role: options.role,
29493
- content: options.content,
29494
- name: options.name ?? null,
29495
- tool_call_id: options.toolCallId ?? null,
29496
- created_at: timestamp,
29497
- status: "completed",
29498
- silent,
29499
- parent_id: forceTopLevel ? null : flow.parentMessageId ?? null,
29500
- depth: forceTopLevel ? 0 : flow.depth
29501
- };
29502
- if (options.beforeMessageId) {
29503
- const cursor = await flow.storage.sql.exec(
29504
- `SELECT created_at FROM messages WHERE id = ?`,
29505
- options.beforeMessageId
29506
- );
29507
- const rows = cursor.toArray();
29508
- const targetMessage = rows[0];
29509
- if (!targetMessage) {
29510
- throw new Error(
29511
- `Message with id ${options.beforeMessageId} not found`
29512
- );
29513
- }
29514
- message.created_at = targetMessage.created_at - 1;
29515
- }
29516
- const currentPrompt = flow.promptPath.length > 0 ? flow.promptPath[flow.promptPath.length - 1] : null;
29517
- await flow.storage.sql.exec(
29518
- `INSERT INTO messages (id, role, content, name, tool_call_id, created_at, status, silent, parent_id, depth, prompt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
29519
- messageId,
29520
- message.role,
29521
- message.content,
29522
- message.name,
29523
- message.tool_call_id,
29524
- message.created_at,
29525
- message.status,
29526
- silent ? 1 : 0,
29527
- message.parent_id,
29528
- message.depth,
29529
- currentPrompt
29530
- );
29531
- if (!silent) {
29532
- flow.messageHistory.push(message);
29533
- } else {
29534
- flow.messageHistory = await _FlowStateSdk.reloadHistory(flow);
29535
- }
29536
- if (flow.emitMessage) {
29537
- flow.emitMessage(message);
29538
- }
29539
- if (options.forceTurn) {
29540
- _FlowStateSdk.forceTurn(flow, options.forceTurn);
29541
- }
29542
- return message;
29715
+ const { injectMessage: injectMessage2 } = await Promise.resolve().then(() => (init_utilities(), utilities_exports));
29716
+ return injectMessage2(flow, options);
29543
29717
  }
29544
29718
  /**
29545
29719
  * Get messages from the thread history
@@ -29628,9 +29802,9 @@ var FlowStateSdk = class _FlowStateSdk {
29628
29802
  /**
29629
29803
  * Reload message history from storage
29630
29804
  *
29631
- * Re-loads the complete message history from the thread's SQLite storage,
29632
- * applying any filter_messages hooks. This is useful when you need to refresh
29633
- * the message history after making changes (e.g., injecting silent messages).
29805
+ * Re-loads the complete message history from the thread's SQLite storage.
29806
+ * This is useful when you need to refresh the message history after making
29807
+ * changes (e.g., injecting messages).
29634
29808
  *
29635
29809
  * @param flow - The current FlowState
29636
29810
  * @returns Array of messages (reloaded from storage)