Skip to content

feat(harness): built-in ask_user tool — model-initiated questions to the user (HITL ask direction) - #2865

Open
Letter2025 wants to merge 7 commits into
agentscope-ai:mainfrom
Letter2025:feat/ask-user-tool
Open

feat(harness): built-in ask_user tool — model-initiated questions to the user (HITL ask direction)#2865
Letter2025 wants to merge 7 commits into
agentscope-ai:mainfrom
Letter2025:feat/ask-user-tool

Conversation

@Letter2025

Copy link
Copy Markdown

Summary

Adds the HITL "ask" direction to AgentScope: the model can proactively ask the user structured questions during a run, complementing the existing user-confirms-tool flow.

A tool whose checkPermissions() returns PermissionDecision.askUser(...) pauses the run with the new GenerateReason.ASK_USER_ASKING — in every PermissionMode (including BYPASS, since it rides the bypass-immune tool self-check) — and is never executed. Callers render the questions and resume with AskUserResult(s) under Msg.METADATA_ASK_USER_RESULTS; the framework formats the answers into the ask_user tool result so the model reads them on the next reasoning iteration.

The harness ships a built-in ask_user tool (JSON-schema'd questions[]: id/header/question/type single|multiple|free|secret/options/required), enabled via HarnessAgent.Builder.enableAskUser() (opt-in, off by default — no behavior change for existing builders).

Motivation

HITL today only answers "may this tool run?" (PERMISSION_ASKING + RequireUserConfirmEvent). There is no way for the model to request information from the user. This is a standard capability in current coding agents (OpenAI Codex request_user_input, Claude Code / Agent SDK AskUserQuestion, Cline, DeepSeek Harness ask_user_question) and has been requested in:

Core changes (agentscope-core)

Area Change
PermissionBehavior new ASK_USER constant (not registerable as a rule; produced only by a tool's checkPermissions())
PermissionDecision new askUser(String) factory
PermissionEngine ASK_USER short-circuits immediately in the bypass-immune tool-sef-check phase (not swallowed by rule/BYPASS handling)
ReActAgent gate routes ASK_USER to a dedicated pending list; actingStream emits RequireUserAskEvent + RequestStopEvent(ASK_USER_ASKING); acting() returns the pausing assistant Msg; resumes with answers via validateAndAcceptAskUserResults which formats them into the tool result without executing it
Events new RequireUserAskEvent / UserAskResultEvent (replyId-correlated, parallel to the confirm flow; registered in AgentEvent subtypes)
GenerateReason new ASK_USER_ASKING

Harness changes (agentscope-harness)

  • Built-in AskUserTool in io.agentscope.harness.agent.tool (name ask_user, checkPermissions()askUser(...))
  • HarnessAgent.Builder.enableAskUser() (no-arg + boolean overload), registers the tool at build()

Docs & examples

  • docs/v1/{en,zh}/docs/task/hitl.md — new "Ask the user for input" section
  • docs/v2/{en,zh}/docs/building-blocks/permission-system.md — new "Ask the user (model-initiated questions)" section (incl. DONT_ASK note)
  • agentscope-examples/.../hitl/AskUserHITLExample.java — runnable interactive example

Tests

  • ReActAgentAskUserTest (core): pause with ASK_USER_ASKING, tool not executed, pause under BYPASS, resume → answers visible in next reasoning, resume-without-answers guidance error, streaming RequireUserAskEvent/UserAskResultEvent correlation, answer formatting (scalar/list/selected+text/skipped)
  • HarnessAgentAskUserTest (harness): enableAskUser() registers the tool and pauses; BYPASS still pauses; full resume round-trip (answer reaches the model, ask_user not executed); tool self-description
  • Existing HITL/permission suites stay green

Compatibility

Opt-in feature: existing HarnessAgent/ReActAgent builders are unaffected (tool not registered, no new gate entries, GenerateReason gains one value). ASK_USER is not a rule behavior, so no permission-config surface changes.

Closes #2860

Adds the HITL ask direction: a tool whose checkPermissions() returns
PermissionDecision.askUser(...) pauses the run with a new
GenerateReason.ASK_USER_ASKING (in every permission mode, including BYPASS)
and is never executed; callers render the questions and resume with
AskUserResult(s) under Msg.METADATA_ASK_USER_RESULTS.

Core:
- PermissionBehavior.ASK_USER + PermissionDecision.askUser()
- PermissionEngine: ASK_USER short-circuits the bypass-immune tool self-check
- ReActAgent: gate routes ASK_USER to a dedicated pending list; actingStream
  emits RequireUserAskEvent + RequestStopEvent(ASK_USER_ASKING); acting()
  returns the pausing assistant Msg unchanged; doCallInner accepts and
  formats AskUserResults into the ask_user tool result on resume
- New events: RequireUserAskEvent / UserAskResultEvent (replyId-correlated)
- GenerateReason.ASK_USER_ASKING

Harness:
- Built-in AskUserTool (ask_user, JSON-schema'd questions[])
- HarnessAgent.Builder.enableAskUser() (opt-in, off by default)

Docs + examples + tests:
- v1 hitl.md / v2 permission-system.md (en+zh) document the ask direction
- AskUserHITLExample runnable example
- ReActAgentAskUserTest (core) + HarnessAgentAskUserTest (harness) cover
  pause under BYPASS, batch questions, skip semantics, resume without
  executing the tool

Closes agentscope-ai#2860
… new events

- AskUserResult gains @JsonCreator/@JsonProperty so UserAskResultEvent
  decodes through the RemoteEventCodec AGENT_EVENT passthrough
- RemoteEventCodecPassthroughTest covers REQUIRE_USER_ASK / USER_ASK_RESULT
- PermissionBehaviorTest expects five behaviors (ASK_USER added)
…ests

Default memory hooks schedule background flush/maintenance after each call;
disabling them keeps the AskUser harness tests deterministic and free of
post-call background threads in the shared surefire JVM.
…7153)

HarnessAgentSubagentStreamTest.call_localSubagent_returnsReplyWithoutStreaming
fails on the CI runner with 'Failed to close extension context' at @tempdir
teardown. The same failure occurred on main's guava-bump CI (no relation to
this PR); the test passes locally. No code change in this commit.
@Letter2025

Copy link
Copy Markdown
Author

Non-blocking note on CI: the remaining build failures are a pre-existing harness test flake, not caused by this PR.

The failing jobs report Failed to close extension context / Failed to delete temp directory at JUnit @TempDir teardown time, in a different test class on every run:

  • a7250f9HarnessAgentSubagentStreamTest + HarnessAgentIntegrationExampleTest
  • 64c83f3HarnessAgentDynamicHookBuilderTest
  • a0d565aHarnessAgentSubagentStreamTest (again)
  • 75c7381JsonSessionDefaultLocationTest

The same failure pattern hit main's own CI on an unrelated commit (guava bump, run 32981277153HarnessAgentSubagentStreamTest.call_localSubagent_returnsReplyWithoutStreaming -> Failed to close extension context), and all of these classes pass locally (and pass on CI in runs where they are not the flaky one). Root-cause hypothesis: harness default memory flush/maintenance middleware keeps writing workspace files on a background scheduler after the test body completes, racing @TempDir cleanup.

The deterministic failures caused by this PR were fixed and verified:

  • added ASK_USER to PermissionBehaviorTest expectations
  • added REQUIRE_USER_ASK / USER_ASK_RESULT samples to RemoteEventCodecPassthroughTest
  • added @JsonCreator/@JsonProperty to AskUserResult so UserAskResultEvent round-trips through RemoteEventCodec

Happy to rebase/adapt if maintainers prefer a different approach.

@Letter2025

Copy link
Copy Markdown
Author

Hi @mikemikimike @helloworldtang @wzq-xzwj — could someone rerun the failed CI jobs on this PR? The failure is a pre-existing harness @TempDir teardown flake, unrelated to the change itself:

  • The only failing test in the last runs is HarnessAgentSubagentStreamTest.call_localSubagent_returnsReplyWithoutStreaming (and in earlier runs HarnessAgentDynamicHookBuilderTest / JsonSessionDefaultLocationTest) — each failing at Failed to close extension context / Failed to delete temp directory during teardown, with the test bodies passing.
  • The same failure appeared on main's own CI for the unrelated guava-bump commit (run 32981277153: same test class, same Failed to close extension context), and all these classes pass locally and on CI in runs where they are not the flaky one.
  • Suspected root cause: the harness default memory flush/maintenance middleware keeps writing workspace files on a background scheduler after the test body completes, racing @TempDir cleanup. Happy to file/adjust a follow-up if a global fix is preferred.

This branch already verified locally: full core + harness suites pass (only known local-only errors: DangerousPathBypassTest symlink cases on Windows without symlink privileges).

@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

- AskUserEventsTest: full-arg constructors, getters, Jackson round-trips and
  null-safe payloads for RequireUserAskEvent / UserAskResultEvent; @JsonCreator
  round-trip + edge branches of AskUserResult (empty list, selected-only,
  text-only, skipped=false, scalar values, multi-entry)
- ReActAgentAskUserTest: resume validation error branches (duplicate answer
  id, unknown/stale tool call id)
- HarnessAgentAskUserTest: AskUserTool.callAsync fallback covers the
  interactive placeholder; AskUserTool fallback text now carries the real
  metadata key value
@oss-maintainer

Copy link
Copy Markdown
Collaborator

⚠️ Merge conflict detected

This PR has conflicts with the main branch and cannot be merged. Please rebase or merge main into your branch and resolve the conflicts:

git fetch origin
git checkout feat/ask-user-tool
git rebase origin/main
# resolve conflicts, then:
git push --force-with-lease

This is a one-time reminder. Feel free to @mention me for a re-review after conflicts are resolved.


Automated notification by github-manager-bot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Built-in ask_user tool - model-initiated questions to the user (HITL ask direction)

2 participants