@cognipeer/agent-sdk 0.6.5 → 0.6.6

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/README.md CHANGED
@@ -37,7 +37,8 @@ Highlights:
37
37
  - **Unified reasoning surface** – one `reasoning` config controls provider-native reasoning and post-tool plain-text reflections.
38
38
  - **Structured output** – provide a Zod schema and the agent injects a finalize tool to capture JSON deterministically.
39
39
  - **Multi-agent and handoffs** – wrap agents as tools or transfer control mid-run with `asTool` / `asHandoff`.
40
- - **Usage + events** – normalize provider usage, surface `tool_call`, `plan`, `summarization`, `reflection`, `metadata`, and `handoff` events.
40
+ - **Human-in-the-loop** – `needsApproval` tools pause for review; `humanInTheLoop.askUser` enables a built-in `ask_user_question` tool that pauses with a structured multi-choice prompt and resumes from the user's answer.
41
+ - **Usage + events** – normalize provider usage, surface `tool_call`, `tool_approval`, `user_question`, `plan`, `summarization`, `reflection`, `metadata`, and `handoff` events.
41
42
  - **Structured tracing** – optional per-invoke JSON traces with metadata, payload capture, and pluggable sinks (file, HTTP, Cognipeer, custom).
42
43
 
43
44
  ## What’s inside
@@ -267,6 +268,7 @@ console.log(res.content);
267
268
  - **Planning discipline** – when planning is enabled the system prompt distinguishes full plan writes from incremental plan updates and emits `plan` events as todos change.
268
269
  - **Structured output** – supply `outputSchema` and the framework adds a hidden `response` finalize tool; parsed JSON is returned as `result.output`.
269
270
  - **Multi-agent orchestration** – reuse agents via `agent.asTool({ toolName })` or perform handoffs that swap runtimes mid-execution.
271
+ - **Ask the user** – opt in with `humanInTheLoop: { askUser: true }` to give the model an `ask_user_question` tool that pauses the run; resume by calling `agent.resolveUserQuestion(state, { id, answers })`. The global `allowFreeText` flag controls whether "Other" / typed answers are allowed.
270
272
  - **MCP + LangChain tools** – any object satisfying the minimal tool interface works; LangChain’s `Tool` implementations plug in directly.
271
273
  - **Vision input** – message parts accept base64 or URL images for multimodal requests.
272
274
  - **Observability hooks** – `config.onEvent` surfaces tool lifecycle, summarization, reflection, metadata, and final answer events for streaming UIs or CLIs.
@@ -285,6 +287,7 @@ Examples live under `examples/` with per-folder READMEs. Build the package first
285
287
  | `summarize-context/` | Summaries + `get_tool_response` raw retrieval. |
286
288
  | `structured-output/` | Zod schema finalize tool and parsed outputs. |
287
289
  | `rewrite-summary/` | Continue conversations after summaries are injected. |
290
+ | `ask-user/` | Built-in `ask_user_question` tool, structured multi-choice prompt, resume with user's answer. |
288
291
  | `multi-agent/` | Delegating between agents via `asTool`. |
289
292
  | `handoff/` | Explicit runtime handoffs. |
290
293
  | `mcp-tavily/` | MCP remote tool discovery. |
@@ -349,7 +352,9 @@ Exported helpers (`agent-sdk/src/index.ts`):
349
352
  **Utilities:**
350
353
  - `buildSystemPrompt(extra?, planning?, name?)`
351
354
  - `setTokenCounter`, `getTokenCounter`, `defaultTokenCounter` – swap in a real tokenizer (tiktoken, etc.) globally or per-invoke
352
- - Node factories (`nodes/*`), context helpers, token utilities, and full TypeScript types (`SmartAgentOptions`, `SmartState`, `AgentInvokeResult`, etc.).
355
+ - `resolveToolApprovalState`, `resolveUserQuestionState` – pure helpers behind `agent.resolveToolApproval` / `agent.resolveUserQuestion` for callers that want to apply a resolution outside the agent factory.
356
+ - `createAskUserQuestionTool(stateRef, { allowFreeText, promptOverride })` – exposed for advanced users that want to attach the human-in-the-loop tool manually instead of via `humanInTheLoop.askUser`.
357
+ - Node factories (`nodes/*`), context helpers, token utilities, and full TypeScript types (`SmartAgentOptions`, `SmartState`, `AgentInvokeResult`, `PendingUserQuestion`, `UserQuestionResolution`, etc.).
353
358
 
354
359
  `SmartAgentOptions` accepts the usual suspects (`model`, `tools`, `limits`, `runtimeProfile`, `customProfile`, `useTodoList`, `summarization`, `reasoning`, `usageConverter`, `tracing`, `tokenCounter`, `costEstimator`). See `docs/api/` for detailed type references.
355
360
 
package/dist/index.cjs CHANGED
@@ -753,6 +753,7 @@ function createResolverNode() {
753
753
  toolHistory: state.toolHistory || [],
754
754
  toolHistoryArchived: state.toolHistoryArchived || [],
755
755
  pendingApprovals: state.pendingApprovals || [],
756
+ pendingUserQuestions: state.pendingUserQuestions || [],
756
757
  plan: state.plan || null,
757
758
  planVersion: state.planVersion || 0
758
759
  };
@@ -2594,13 +2595,16 @@ function createToolsNode(initialTools, opts) {
2594
2595
  const traceSession = state.ctx?.__traceSession;
2595
2596
  const pendingApprovals = Array.isArray(state.pendingApprovals) ? state.pendingApprovals.map((entry) => ({ ...entry })) : [];
2596
2597
  const pendingByCallId = new Map(pendingApprovals.map((entry) => [entry.toolCallId, entry]));
2598
+ const pendingUserQuestions = Array.isArray(state.pendingUserQuestions) ? state.pendingUserQuestions.map((entry) => ({ ...entry })) : [];
2597
2599
  let awaitingApproval = false;
2600
+ let awaitingUserQuestion = false;
2598
2601
  for (const tool of toolByName.values()) {
2599
2602
  const anyTool = tool;
2600
2603
  if (anyTool._stateRef && typeof anyTool._stateRef === "object") {
2601
2604
  anyTool._stateRef.toolHistory = state.toolHistory;
2602
2605
  anyTool._stateRef.toolHistoryArchived = state.toolHistoryArchived;
2603
2606
  anyTool._stateRef.pendingApprovals = pendingApprovals;
2607
+ anyTool._stateRef.pendingUserQuestions = pendingUserQuestions;
2604
2608
  anyTool._stateRef.messages = state.messages;
2605
2609
  anyTool._stateRef.ctx = state.ctx || (state.ctx = {});
2606
2610
  anyTool._stateRef.__onEvent = onEvent;
@@ -2674,6 +2678,7 @@ function createToolsNode(initialTools, opts) {
2674
2678
  const toolStateRef = tool._stateRef && typeof tool._stateRef === "object" ? tool._stateRef : null;
2675
2679
  if (toolStateRef) {
2676
2680
  toolStateRef.pendingApprovals = pendingApprovals;
2681
+ toolStateRef.pendingUserQuestions = pendingUserQuestions;
2677
2682
  toolStateRef.ctx = state.ctx || (state.ctx = {});
2678
2683
  toolStateRef.__currentToolCallId = toolCallId;
2679
2684
  delete toolStateRef.__awaitingApproval;
@@ -2885,6 +2890,22 @@ function createToolsNode(initialTools, opts) {
2885
2890
  approval: pendingApproval
2886
2891
  };
2887
2892
  }
2893
+ if (output && typeof output === "object" && output.__awaitingUserQuestion) {
2894
+ if (toolStateRef?.ctx && typeof toolStateRef.ctx === "object") {
2895
+ state.ctx = toolStateRef.ctx;
2896
+ }
2897
+ const pendingQuestion = pendingUserQuestions.find(
2898
+ (entry) => entry.toolCallId === output.toolCallId || entry.id === output.id
2899
+ );
2900
+ if (!pendingQuestion) {
2901
+ throw new Error(`Tool ${toolName} requested a user question but no pending question entry was recorded.`);
2902
+ }
2903
+ awaitingUserQuestion = true;
2904
+ return {
2905
+ status: "awaiting_user_question",
2906
+ userQuestion: pendingQuestion
2907
+ };
2908
+ }
2888
2909
  if (output && typeof output === "object" && output.__finalStructuredOutput) {
2889
2910
  if (!state.ctx) state.ctx = {};
2890
2911
  state.ctx.__structuredOutputParsed = output.data;
@@ -3022,15 +3043,19 @@ function createToolsNode(initialTools, opts) {
3022
3043
  return false;
3023
3044
  };
3024
3045
  for (const idx of approvalIndices) {
3025
- if (awaitingApproval || shouldStop) break;
3046
+ if (awaitingApproval || awaitingUserQuestion || shouldStop) break;
3026
3047
  if (checkCancellationStop()) break;
3027
3048
  const result = await runOne(planned[idx], orderedSlots[idx]);
3028
3049
  if (result.status === "awaiting_approval") {
3029
3050
  awaitingApproval = true;
3030
3051
  break;
3031
3052
  }
3053
+ if (result.status === "awaiting_user_question") {
3054
+ awaitingUserQuestion = true;
3055
+ break;
3056
+ }
3032
3057
  }
3033
- if (!awaitingApproval && !shouldStop && parallelIndices.length > 0) {
3058
+ if (!awaitingApproval && !awaitingUserQuestion && !shouldStop && parallelIndices.length > 0) {
3034
3059
  const concurrency = Math.max(1, Math.min(limits.maxParallelTools, parallelIndices.length));
3035
3060
  let cursor = 0;
3036
3061
  const workerError = {};
@@ -3038,7 +3063,7 @@ function createToolsNode(initialTools, opts) {
3038
3063
  for (let w = 0; w < concurrency; w++) {
3039
3064
  workers.push((async () => {
3040
3065
  while (true) {
3041
- if (workerError.err || shouldStop || awaitingApproval) return;
3066
+ if (workerError.err || shouldStop || awaitingApproval || awaitingUserQuestion) return;
3042
3067
  const i = cursor++;
3043
3068
  if (i >= parallelIndices.length) return;
3044
3069
  if (checkCancellationStop()) return;
@@ -3049,6 +3074,10 @@ function createToolsNode(initialTools, opts) {
3049
3074
  awaitingApproval = true;
3050
3075
  return;
3051
3076
  }
3077
+ if (result.status === "awaiting_user_question") {
3078
+ awaitingUserQuestion = true;
3079
+ return;
3080
+ }
3052
3081
  } catch (err) {
3053
3082
  workerError.err = err;
3054
3083
  return;
@@ -3111,11 +3140,12 @@ function createToolsNode(initialTools, opts) {
3111
3140
  toolCache: state.toolCache,
3112
3141
  // Explicitly propagate ctx so callers using `{ ...state, ...delta }`
3113
3142
  // do not silently capture a stale ctx reference. Mutations to
3114
- // __summarizationExhausted / __awaitingApproval / __structuredOutputParsed
3115
- // are made on this object during execution.
3143
+ // __summarizationExhausted / __awaitingApproval / __awaitingUserQuestion /
3144
+ // __structuredOutputParsed are made on this object during execution.
3116
3145
  ctx: state.ctx,
3117
3146
  agent: state.agent,
3118
- pendingApprovals
3147
+ pendingApprovals,
3148
+ pendingUserQuestions
3119
3149
  };
3120
3150
  };
3121
3151
  }
@@ -3581,6 +3611,226 @@ function resolveToolApprovalState(state, resolution) {
3581
3611
  };
3582
3612
  }
3583
3613
 
3614
+ // src/utils/userQuestions.ts
3615
+ var cloneQuestions = (questions) => Array.isArray(questions) ? questions.map((entry) => ({ ...entry })) : [];
3616
+ function validateAnswers(entry, answers, cancelled) {
3617
+ if (cancelled) {
3618
+ return answers || {};
3619
+ }
3620
+ const allowFreeText = entry.allowFreeText !== false;
3621
+ const out = {};
3622
+ const provided = answers || {};
3623
+ for (const question of entry.questions) {
3624
+ const required = question.required !== false;
3625
+ const key = question.question;
3626
+ const answer = provided[key] ?? provided[question.header || ""];
3627
+ if (!answer) {
3628
+ if (required) {
3629
+ throw new Error(`Missing answer for required question: "${key}".`);
3630
+ }
3631
+ continue;
3632
+ }
3633
+ const values = Array.isArray(answer.values) ? answer.values.filter((v) => typeof v === "string") : [];
3634
+ const freeText = typeof answer.freeText === "string" ? answer.freeText.trim() : "";
3635
+ const notes = typeof answer.notes === "string" ? answer.notes : void 0;
3636
+ if (freeText && !allowFreeText) {
3637
+ throw new Error(
3638
+ `Free-text answers are disabled for this agent (question: "${key}"). Pick one of the provided options.`
3639
+ );
3640
+ }
3641
+ if (values.length === 0 && !freeText) {
3642
+ if (required) {
3643
+ throw new Error(`Question "${key}" requires either a selected option or free-text answer.`);
3644
+ }
3645
+ continue;
3646
+ }
3647
+ if (values.length > 1 && !question.multiSelect) {
3648
+ throw new Error(`Question "${key}" is single-select but received multiple values.`);
3649
+ }
3650
+ if (Array.isArray(question.options) && question.options.length > 0 && values.length > 0) {
3651
+ const allowedValues = new Set(
3652
+ question.options.map((option) => option.value ?? option.label)
3653
+ );
3654
+ for (const value of values) {
3655
+ if (!allowedValues.has(value)) {
3656
+ if (!allowFreeText) {
3657
+ throw new Error(
3658
+ `Value "${value}" is not one of the allowed options for question "${key}".`
3659
+ );
3660
+ }
3661
+ }
3662
+ }
3663
+ }
3664
+ out[key] = { values, ...freeText ? { freeText } : {}, ...notes ? { notes } : {} };
3665
+ }
3666
+ return out;
3667
+ }
3668
+ function buildToolResponseContent(entry, answers, cancelled, notes) {
3669
+ if (cancelled) {
3670
+ return JSON.stringify({
3671
+ status: "cancelled",
3672
+ message: notes || "User dismissed the question without answering."
3673
+ });
3674
+ }
3675
+ const payload = {
3676
+ status: "answered",
3677
+ answers: entry.questions.map((question) => {
3678
+ const key = question.question;
3679
+ const ans = answers[key];
3680
+ return {
3681
+ question: key,
3682
+ header: question.header,
3683
+ values: ans?.values ?? [],
3684
+ freeText: ans?.freeText,
3685
+ notes: ans?.notes
3686
+ };
3687
+ }),
3688
+ ...notes ? { reviewerNotes: notes } : {}
3689
+ };
3690
+ return JSON.stringify(payload);
3691
+ }
3692
+ function resolveUserQuestionState(state, resolution) {
3693
+ const pending = cloneQuestions(state.pendingUserQuestions);
3694
+ const matchIndex = pending.findIndex(
3695
+ (entry2) => entry2.id === resolution.id || entry2.toolCallId === resolution.id
3696
+ );
3697
+ if (matchIndex === -1) {
3698
+ throw new Error(`Pending user question not found for id: ${resolution.id}`);
3699
+ }
3700
+ const entry = { ...pending[matchIndex] };
3701
+ if (entry.status === "answered" || entry.status === "executed" || entry.status === "cancelled") {
3702
+ throw new Error(`User question ${entry.id} already resolved (status: ${entry.status}).`);
3703
+ }
3704
+ const cancelled = resolution.cancelled === true;
3705
+ const validated = validateAnswers(entry, resolution.answers, cancelled);
3706
+ entry.status = cancelled ? "cancelled" : "answered";
3707
+ entry.answers = validated;
3708
+ entry.cancelled = cancelled || void 0;
3709
+ entry.answeredBy = resolution.answeredBy;
3710
+ entry.notes = resolution.notes;
3711
+ entry.answeredAt = (/* @__PURE__ */ new Date()).toISOString();
3712
+ pending[matchIndex] = entry;
3713
+ const ctx = { ...state.ctx || {} };
3714
+ delete ctx.__awaitingUserQuestion;
3715
+ if (ctx.__resumeStage === "tools") {
3716
+ delete ctx.__resumeStage;
3717
+ }
3718
+ ctx.__userQuestionResolved = { id: entry.id, status: entry.status };
3719
+ const toolMessage = {
3720
+ role: "tool",
3721
+ name: "ask_user_question",
3722
+ tool_call_id: entry.toolCallId,
3723
+ content: buildToolResponseContent(entry, validated, cancelled, resolution.notes)
3724
+ };
3725
+ return {
3726
+ ...state,
3727
+ pendingUserQuestions: pending,
3728
+ messages: [...state.messages || [], toolMessage],
3729
+ ctx
3730
+ };
3731
+ }
3732
+ var DEFAULT_DESCRIPTION_WITH_FREETEXT = "Pause the agent and surface a structured prompt to the human user when you genuinely cannot proceed without their input. Group up to 4 closely related questions into a single call. Prefer concrete options the user can pick over open-ended free-text \u2014 list `options` whenever you can anticipate the choices. The user may type a custom answer for any question. Do not use this tool for things you can deduce from the conversation, the system prompt, or another tool. After the user answers, their selections come back as the tool result.";
3733
+ var DEFAULT_DESCRIPTION_NO_FREETEXT = "Pause the agent and surface a structured multi-choice prompt to the human user when you genuinely cannot proceed without their input. Group up to 4 closely related questions into a single call. Every question MUST include at least two `options` \u2014 free-text answers are disabled for this agent. Do not use this tool for things you can deduce from the conversation, the system prompt, or another tool. After the user picks, their selections come back as the tool result.";
3734
+ function buildOptionSchema() {
3735
+ return zod.z.object({
3736
+ label: zod.z.string().min(1).describe("Display text the user sees"),
3737
+ value: zod.z.string().min(1).optional().describe("Value returned in the answer; defaults to label"),
3738
+ description: zod.z.string().optional(),
3739
+ preview: zod.z.string().optional().describe("Optional code / ASCII preview rendered when this option is focused")
3740
+ }).strict();
3741
+ }
3742
+ function buildQuestionSchema(allowFreeText) {
3743
+ const optionSchema = buildOptionSchema();
3744
+ const baseShape = {
3745
+ question: zod.z.string().min(1).describe("Full question shown to the user"),
3746
+ header: zod.z.string().max(40).optional().describe("Short chip / tag rendered next to the question (max ~12 chars recommended)"),
3747
+ multiSelect: zod.z.boolean().optional().describe("Allow multiple selections. Default: false"),
3748
+ placeholder: zod.z.string().optional(),
3749
+ required: zod.z.boolean().optional().describe("Default: true")
3750
+ };
3751
+ if (allowFreeText) {
3752
+ return zod.z.object({
3753
+ ...baseShape,
3754
+ options: zod.z.array(optionSchema).min(2).max(12).optional()
3755
+ }).strict();
3756
+ }
3757
+ return zod.z.object({
3758
+ ...baseShape,
3759
+ options: zod.z.array(optionSchema).min(2).max(12).describe("Required: free-text answers are disabled for this agent")
3760
+ }).strict();
3761
+ }
3762
+ function createAskUserQuestionTool(stateRef, config) {
3763
+ const allowFreeText = config.allowFreeText !== false;
3764
+ const description = config.promptOverride ?? (allowFreeText ? DEFAULT_DESCRIPTION_WITH_FREETEXT : DEFAULT_DESCRIPTION_NO_FREETEXT);
3765
+ const questionSchema = buildQuestionSchema(allowFreeText);
3766
+ const askUser = createTool({
3767
+ name: "ask_user_question",
3768
+ description,
3769
+ schema: zod.z.object({
3770
+ questions: zod.z.array(questionSchema).min(1).max(4).describe("1-4 related questions to ask in a single pause.")
3771
+ }).strict(),
3772
+ func: async ({ questions }) => {
3773
+ const ref = askUser._stateRef || (askUser._stateRef = {});
3774
+ const onEvent = ref.__onEvent;
3775
+ if (!ref.ctx) ref.ctx = {};
3776
+ const ctx = ref.ctx;
3777
+ const toolCallId = ref.__currentToolCallId || `ask_user_${nanoid.nanoid(6)}`;
3778
+ if (!ref.pendingUserQuestions) ref.pendingUserQuestions = [];
3779
+ const pendingList = ref.pendingUserQuestions;
3780
+ const entry = {
3781
+ id: nanoid.nanoid(),
3782
+ toolCallId,
3783
+ toolName: "ask_user_question",
3784
+ questions,
3785
+ status: "pending",
3786
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
3787
+ allowFreeText
3788
+ };
3789
+ pendingList.push(entry);
3790
+ ctx.__awaitingUserQuestion = {
3791
+ id: entry.id,
3792
+ toolCallId: entry.toolCallId,
3793
+ requestedAt: entry.requestedAt
3794
+ };
3795
+ ctx.__resumeStage = "tools";
3796
+ onEvent?.({
3797
+ type: "user_question",
3798
+ status: "pending",
3799
+ id: entry.id,
3800
+ toolCallId: entry.toolCallId,
3801
+ questions: entry.questions,
3802
+ allowFreeText
3803
+ });
3804
+ recordTraceEvent(ctx.__traceSession, {
3805
+ type: "user_question",
3806
+ label: "Ask User",
3807
+ actor: { scope: "agent", name: "ask_user_question", role: "user_interaction" },
3808
+ status: "success",
3809
+ sections: [
3810
+ {
3811
+ kind: "metadata",
3812
+ label: "Pending Question",
3813
+ data: {
3814
+ id: entry.id,
3815
+ toolCallId: entry.toolCallId,
3816
+ questions: entry.questions,
3817
+ allowFreeText
3818
+ }
3819
+ }
3820
+ ]
3821
+ });
3822
+ return {
3823
+ __awaitingUserQuestion: true,
3824
+ id: entry.id,
3825
+ toolCallId: entry.toolCallId
3826
+ };
3827
+ }
3828
+ });
3829
+ askUser._stateRef = stateRef;
3830
+ askUser.__source = "human-in-the-loop";
3831
+ return askUser;
3832
+ }
3833
+
3584
3834
  // src/agent.ts
3585
3835
  init_utilTokens();
3586
3836
 
@@ -3792,6 +4042,21 @@ function createAgent(opts) {
3792
4042
  const resolver = createResolverNode();
3793
4043
  const agentCore = createAgentCoreNode(opts);
3794
4044
  const toolsBase = [...opts.tools ?? []];
4045
+ const askUserConfig = (() => {
4046
+ const raw = opts.humanInTheLoop?.askUser;
4047
+ if (!raw) return void 0;
4048
+ if (raw === true) return { enabled: true, allowFreeText: true };
4049
+ return {
4050
+ enabled: true,
4051
+ allowFreeText: raw.allowFreeText !== false,
4052
+ promptOverride: raw.promptOverride
4053
+ };
4054
+ })();
4055
+ const askUserOnQuestion = typeof opts.humanInTheLoop?.askUser === "object" ? opts.humanInTheLoop?.askUser?.onQuestion : void 0;
4056
+ const askUserStateRef = askUserConfig ? { pendingUserQuestions: void 0, ctx: void 0, __onEvent: void 0, __currentToolCallId: void 0 } : void 0;
4057
+ if (askUserConfig && askUserStateRef) {
4058
+ toolsBase.push(createAskUserQuestionTool(askUserStateRef, askUserConfig));
4059
+ }
3795
4060
  const soManager = opts.outputSchema ? new StructuredOutputManager(opts.outputSchema, resolveStrategy(opts.model)) : void 0;
3796
4061
  if (soManager) {
3797
4062
  const modelCapabilities = getModelCapabilities(opts.model);
@@ -3836,7 +4101,8 @@ function createAgent(opts) {
3836
4101
  useTodoList: void 0,
3837
4102
  outputSchema: opts.outputSchema,
3838
4103
  responseFormat: soManager?.getResponseFormat(),
3839
- tracing: opts.tracing
4104
+ tracing: opts.tracing,
4105
+ humanInTheLoop: askUserConfig ? { askUser: askUserConfig } : void 0
3840
4106
  };
3841
4107
  const summarizationThreshold = getActiveSummarizationThreshold(opts);
3842
4108
  async function runLoop(initial, config, emit) {
@@ -4126,6 +4392,7 @@ function createAgent(opts) {
4126
4392
  state = { ...state, ...await toolsNode(state) };
4127
4393
  onProgress?.({ stage: "tools", message: "Tools finished" });
4128
4394
  if (state.ctx?.__awaitingApproval) break;
4395
+ if (state.ctx?.__awaitingUserQuestion) break;
4129
4396
  if (checkpointIfRequested("after_tools")) break;
4130
4397
  if (state.ctx?.__finalizedDueToStructuredOutput) break;
4131
4398
  if (reflectNode && resolvedReasoning?.reflection.enabled) {
@@ -4196,7 +4463,7 @@ function createAgent(opts) {
4196
4463
  }
4197
4464
  const lastAfterLoop = state.messages[state.messages.length - 1];
4198
4465
  if (lastAfterLoop?.role === "tool" && !pausedStage) {
4199
- const isExpectedExit = state.ctx?.__awaitingApproval || state.ctx?.__cancelled || state.ctx?.__finalizedDueToStructuredOutput || state.ctx?.__finalizedDueToToolLimit || state.ctx?.__needsSummarization || state.ctx?.__limitBreached;
4466
+ const isExpectedExit = state.ctx?.__awaitingApproval || state.ctx?.__awaitingUserQuestion || state.ctx?.__cancelled || state.ctx?.__finalizedDueToStructuredOutput || state.ctx?.__finalizedDueToToolLimit || state.ctx?.__needsSummarization || state.ctx?.__limitBreached;
4200
4467
  if (!isExpectedExit) {
4201
4468
  throw new Error(
4202
4469
  "Agent loop terminated with a pending tool response but no subsequent model invocation. This usually indicates a model provider error or exhausted iteration budget."
@@ -4214,7 +4481,17 @@ function createAgent(opts) {
4214
4481
  const { setTokenCounter: setTokenCounter2 } = await Promise.resolve().then(() => (init_utilTokens(), utilTokens_exports));
4215
4482
  setTokenCounter2(userTokenCounter);
4216
4483
  }
4217
- const onEvent = config?.onEvent;
4484
+ const callerOnEvent = config?.onEvent;
4485
+ const onEvent = askUserOnQuestion ? (event) => {
4486
+ if (event && event.type === "user_question") {
4487
+ try {
4488
+ askUserOnQuestion(event);
4489
+ } catch (err) {
4490
+ console.warn("[agent-sdk] humanInTheLoop.askUser.onQuestion error:", err);
4491
+ }
4492
+ }
4493
+ callerOnEvent?.(event);
4494
+ } : callerOnEvent;
4218
4495
  const onProgress = config?.onProgress;
4219
4496
  const onStream = config?.onStream;
4220
4497
  const streamEnabled = config?.stream === true;
@@ -4258,6 +4535,7 @@ function createAgent(opts) {
4258
4535
  metadata: input.metadata,
4259
4536
  ctx,
4260
4537
  pendingApprovals: input.pendingApprovals || [],
4538
+ pendingUserQuestions: input.pendingUserQuestions || [],
4261
4539
  agent: input.agent || runtimeWithInvokeLimits,
4262
4540
  usage: input.usage || { perRequest: [], totals: {} }
4263
4541
  };
@@ -4334,11 +4612,13 @@ function createAgent(opts) {
4334
4612
  return invokeAgent(restoredState, config);
4335
4613
  };
4336
4614
  const resolveToolApproval = (state, resolution) => resolveToolApprovalState(state, resolution);
4615
+ const resolveUserQuestion = (state, resolution) => resolveUserQuestionState(state, resolution);
4337
4616
  const instance = {
4338
4617
  invoke: invokeAgent,
4339
4618
  snapshot: snapshotState,
4340
4619
  resume: resumeAgent,
4341
4620
  resolveToolApproval,
4621
+ resolveUserQuestion,
4342
4622
  asTool: ({ toolName, description, inputDescription }) => {
4343
4623
  const schema = zod.z.object({ input: zod.z.string().describe(inputDescription || "Input for delegated agent") });
4344
4624
  const delegatedTool = createTool({
@@ -5655,7 +5935,20 @@ function createSmartAgent(opts) {
5655
5935
  const userTools = opts.tools ?? [];
5656
5936
  const factoryStateRef = { toolHistory: void 0, toolHistoryArchived: void 0, todoList: void 0, planVersion: 0, adherenceScore: 0 };
5657
5937
  const factoryContextTools = createContextTools(factoryStateRef, { planningEnabled, includeGetToolResponse: false });
5938
+ const askUserConfig = (() => {
5939
+ const raw = opts.humanInTheLoop?.askUser;
5940
+ if (!raw) return void 0;
5941
+ if (raw === true) return { enabled: true, allowFreeText: true };
5942
+ return {
5943
+ enabled: true,
5944
+ allowFreeText: raw.allowFreeText !== false,
5945
+ promptOverride: raw.promptOverride
5946
+ };
5947
+ })();
5658
5948
  const factoryToolsWithoutRecovery = [...userTools, ...factoryContextTools];
5949
+ if (askUserConfig) {
5950
+ factoryToolsWithoutRecovery.push(createAskUserQuestionTool(factoryStateRef, askUserConfig));
5951
+ }
5659
5952
  const soManagerFactory = opts.outputSchema ? new StructuredOutputManager(opts.outputSchema, resolveStrategy(opts.model)) : void 0;
5660
5953
  const modelCapabilities = opts.outputSchema ? getModelCapabilities(opts.model) : void 0;
5661
5954
  const responseFinalizeTool = soManagerFactory && modelCapabilities?.structuredOutput !== "native" ? soManagerFactory.getResponseTool() : void 0;
@@ -5665,6 +5958,7 @@ function createSmartAgent(opts) {
5665
5958
  const recoveryTool = createGetToolResponseTool(ref);
5666
5959
  const base2 = [...userTools, ...ctxTools];
5667
5960
  if (responseFinalizeTool) base2.push(responseFinalizeTool);
5961
+ if (askUserConfig) base2.push(createAskUserQuestionTool(ref, askUserConfig));
5668
5962
  const without = base2;
5669
5963
  const withRec = [...without, recoveryTool];
5670
5964
  return { stateRef: ref, toolsWithoutRecovery: without, toolsWithRecovery: withRec };
@@ -5849,6 +6143,7 @@ function createSmartAgent(opts) {
5849
6143
  snapshot: base.snapshot,
5850
6144
  resume: base.resume,
5851
6145
  resolveToolApproval: base.resolveToolApproval,
6146
+ resolveUserQuestion: base.resolveUserQuestion,
5852
6147
  asTool: base.asTool,
5853
6148
  asHandoff: base.asHandoff,
5854
6149
  __runtime: base.__runtime
@@ -8174,6 +8469,7 @@ exports.countApproxTokens = countApproxTokens;
8174
8469
  exports.countMessagesTokens = countMessagesTokens;
8175
8470
  exports.createAgent = createAgent;
8176
8471
  exports.createAgentNode = createAgentNode;
8472
+ exports.createAskUserQuestionTool = createAskUserQuestionTool;
8177
8473
  exports.createCodeGuardrail = createCodeGuardrail;
8178
8474
  exports.createContextSummarizeNode = createContextSummarizeNode;
8179
8475
  exports.createContextTools = createContextTools;
@@ -8215,6 +8511,7 @@ exports.regexRule = regexRule;
8215
8511
  exports.resolveMemoryStore = resolveMemoryStore;
8216
8512
  exports.resolveStrategy = resolveStrategy;
8217
8513
  exports.resolveToolApprovalState = resolveToolApprovalState;
8514
+ exports.resolveUserQuestionState = resolveUserQuestionState;
8218
8515
  exports.restoreSnapshot = restoreSnapshot;
8219
8516
  exports.runSmartAgentEvalHarness = runSmartAgentEvalHarness;
8220
8517
  exports.setTokenCounter = setTokenCounter;