Skip to content

fix(aistio): dual-name collaboration tools for OpenAI function.name - #3205

Open
tengjiaozhai wants to merge 3 commits into
agentscope-ai:mainfrom
tengjiaozhai:fix/aistio-collaboration-tool-model-names
Open

tengjiaozhai wants to merge 3 commits into
agentscope-ai:mainfrom
tengjiaozhai:fix/aistio-collaboration-tool-model-names

Conversation

@tengjiaozhai

@tengjiaozhai tengjiaozhai commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

AgentScope-Java Version

2.0.4-SNAPSHOT (branch revision; no invented bump in this PR)

Description

OpenAI-compatible APIs require function.name =~ ^[a-zA-Z0-9_-]{1,64}$. Collaboration tools were registered with dotted control-plane wire names (task.get, issue.comment.add, …), so every AgentTask model request against OpenAI/DeepSeek-compatible gateways failed with HTTP 400.
original:https://raw.githubusercontent.com/openai/openai-openapi/refs/heads/main/openapi.yaml
image

Option 1 (adapter dual naming) — this PR:

  1. AgentTaskCollaborationTool keeps two names:
    • wireName (dotted): used for tools/call, READ_ONLY, and terminal run.node.complete / run.node.fail checks
    • modelName via shared toModelName() (total OpenAI-safe sanitizer: '.'→'_' fast path; other illegal chars + length>64 get deterministic hash suffix): returned from getName() for the LLM / OpenAI payload
  2. HarnessAgentTaskStarter.registerCollaborationTools registers and dedupes by model name; lifecycle skip for task.complete / task.fail uses shared wire-name constants (WIRE_TASK_COMPLETE / WIRE_TASK_FAIL). Outcome tool registration identity-checks AgentTaskOutcomeTool and replaces foreign occupants. Dropped collisions are listed in the kickoff prompt; terminal collisions refuse dispatch.
  3. roleInstructions (and kickoff / correction prompts) use model-facing names derived from the same toModelName() mapping so prose cannot drift from registration.
  4. AgentTaskOutcomeTool: task.submit_result → task_submit_result (same mapping); now a ToolBase for identity + permission participation.

Wire dispatch to the control plane is unchanged (still dotted). Option 2 (core Toolkit pattern validation) is left as a follow-up only.

Closes #3153

Relationship to #3151

#3151 only renames the adapter-owned task.submit_result tool. This PR applies the same underscore rule to all collaboration tools plus that outcome tool, so naming stays consistent. If #3151 merges first, this branch should rebase cleanly over the overlapping lines in HarnessAgentTaskStarter / tests; if this lands first, #3151 becomes redundant and can be closed.

Compatibility / release notes

Breaking for callers that hardcode the old outcome tool name: the model-facing name is now task_submit_result (was task.submit_result). Wire/dispatch for dotted collaboration tools is unchanged. Callers, prompts, and resumed sessions that hardcode the old dotted outcome name should use the underscore form.

Permission rules: PermissionEngine accepts both dotted and underscore spellings during rule lookup when one is the '.'→'_' form of the other (e.g. persisted task.submit_result / issue.comment.add still match live task_submit_result / issue_comment_add). No manual migration required for exact-key allow/deny/ask tables stored under the legacy dotted names.

Verification

mvn -pl agentscope-extensions/agentscope-extensions-aistio -am spotless:apply
mvn -pl agentscope-extensions/agentscope-extensions-aistio spotless:check
mvn -pl agentscope-extensions/agentscope-extensions-aistio -am test
  • agentscope-extensions-aistio: 81 tests, 0 failures (dual-name + terminal-wire coverage, outcome-tool identity replace, collision keep-first / terminal refuse, mcp:server.tool sanitizer contract)
  • Upstream dependency modules in the reactor (core/harness/…): build + tests succeeded as part of -am test (includes PermissionEngineTest$DualSpellingLookup)

Checklist

  • Code has been formatted with mvn spotless:apply
  • All tests are passing (mvn test): aistio module 81/0
  • Javadoc comments are complete and follow project conventions
  • Related documentation has been updated (e.g. links, examples, etc.) — N/A (adapter-internal naming; no public docs / CHANGELOG file in repo; documented in Compatibility section above)
  • Code is ready for review

@oss-maintainer oss-maintainer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

The dual-name design (dotted wire names for MCP dispatch, . → _ model names for OpenAI-compatible function.name) is a sound fix for #3153, and the registration / availableActions / terminal-check paths are consistently reworked. However, this cannot be approved in its current state:

Critical: in AgentTaskCollaborationToolTest.discoveresAndCallsTaskScopedMcpTool the escaped quote was dropped from a JSON string literal (+ " a comment","), which leaves the literal unbalanced and makes the test source non-compiling — CI should fail before any of the new assertions run.

Please restore \" on that line and re-run mvn -pl agentscope-extensions/agentscope-extensions-aistio test-compile (the PR reports 76 tests passing, so this looks like it slipped in on the last rebase).

Non-blocking suggestions: an explicit hint about toModelName() collisions (foo.bar_baz vs foo_bar.baz, or a pre-existing task_submit_result shadowing the outcome tool) plus a test pinning that behavior, and a note on documenting the task.submit_result rename in release notes for resumed sessions / external references.

Good first contribution — the fix rationale, shared wire-name constants, and the added dual-name tests are all well done. Once the test file compiles again, re-request review and we can move quickly.


Automated review by github-manager-bot

@tengjiaozhai

Copy link
Copy Markdown
Contributor Author

Soft review items addressed (no further code churn):

  1. PR description — Verification/checklist test counts updated 76 → 77 (collision keep-first pin).
  2. Compatibility / release notes — added a short note that the model-facing outcome name is now task_submit_result (was task.submit_result); wire/dispatch for dotted collaboration tools unchanged; callers/prompts/resumed sessions that hardcode the old dotted name should use the underscore form.
  3. CHANGELOG — repo has no CHANGELOG / RELEASE_NOTES file for user-facing renames, so the PR-body note is the documentation surface for this rename.

Version remains 2.0.3-SNAPSHOT. Code fixes (escaped quote + collision warn) already in 57093bc.

@codecov

codecov Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

@oss-maintainer oss-maintainer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Re-reviewed at 57093bc. Both threads from the previous request are resolved:

  • The dropped \" in the discoversAndCallsTaskScopedMcpTool fixture is restored — the literal is balanced again and CI confirms build (ubuntu/windows-latest) pass on this commit.
  • The model-name collision suggestion was taken further than asked: registerCollaborationTools now logs a warning identifying the skipped wireName/modelName pair, and collidingModelNameKeepsFirstRegisteredTool pins the keep-first semantics (including that the pre-existing local tool is not replaced).

No new issues found in the delta. Nice turnaround — approving.


Automated review by github-manager-bot

@oss-maintainer

Copy link
Copy Markdown
Collaborator

⚠️ Merge conflict detected

This PR currently conflicts with main (mergeable=CONFLICTING), so it cannot be merged even though the code review is done. Please rebase or merge main into your branch and resolve the conflicts:

git fetch origin
git checkout fix/aistio-collaboration-tool-model-names
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 once the conflicts are resolved.


Automated notification by github-manager-bot

Rebased onto upstream main (c0d03cc). Keep wireName/modelName dual naming,
collision warn, escaped quote in tests, and main's ConfirmResult denyMessage
HITL path.
@tengjiaozhai
tengjiaozhai force-pushed the fix/aistio-collaboration-tool-model-names branch from 57093bc to f838e87 Compare September 21, 2026 17:02
@tengjiaozhai

Copy link
Copy Markdown
Contributor Author

Rebased onto current upstream main @ c0d03ccea95f6de52b3e1cdfa682af66f06dd705.

  • New head: f838e87aa65ae4fca8e791f82dc93179745137f6
  • Conflicts resolved in HarnessAgentTaskStarter.java and HarnessAgentTaskOutcomeTest.java (kept dual-naming wireName/modelName + collision warn + escaped quote; folded in main’s ConfirmResult(..., denyMessage) HITL path and its test)
  • Local verification: spotless:check clean; agentscope-extensions-aistio 78 tests / 0 failures
  • mergeable is no longer dirty

Ready for re-review, cc @oss-maintainer

@oss-maintainer oss-maintainer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Re-review after the rebase onto c0d03ccea. The dual-name design is intact and the conflict resolution looks correct: wireName (dotted) drives MCP dispatch, isReadOnly(), and the terminal-action checks, while getName() now returns the OpenAI-safe modelName; the wire-name constants are used for the task.complete / task.fail exclusion, and ConfirmResult(..., decision.denyMessage()) matches main's current 4-arg HITL signature. The added tests (modelNameUsesUnderscoresWhileWireNameKeepsDots, terminalWireNamesStillTriggerMarkTerminalCommitted, collidingModelNameKeepsFirstRegisteredTool, sameToolkitRegistersOutcomeToolOnlyOnce) cover the right seams, including asserting that params.name on the wire stays dotted.

Not re-approving this round: once normalization is in play, name presence in the toolkit is no longer proof of tool identity, and the outcome tool relies on exactly that proof — plus two compatibility questions created by the rename.

Findings

  • [Critical] adapter/HarnessAgentTaskStarter.java:440 — the contains(submitResultModelName) guard treats any tool named task_submit_result as the adapter's outcome tool. If a foreign tool holds that name, AgentTaskOutcomeTool is never registered, no outcome can be submitted, and runToOutcome spins on its "turn ended without a business outcome" nudge until the budget is exhausted — silently, unlike the collaboration-loop collision which does log. Check identity (getTool(name) instanceof AgentTaskOutcomeTool) instead.
  • [Warning] adapter/AgentTaskCollaborationTool.java:97 — toModelName normalizes only ., so the ^[a-zA-Z0-9_-]+$ property claimed in the class comment holds just for dotted names; core already has a stricter sanitizer in SubAgentTool.resolveToolName (^[a-zA-Z0-9_-]{1,64}$ with a hash fallback) that would also shrink the collision surface.
  • [Warning] adapter/AgentTaskOutcomeTool.java:32 — task.submit_result → task_submit_result breaks persisted permission rules, because PermissionEngine.rulesFor(...) is an exact key lookup on tool.getName() over the allow_rules / deny_rules / ask_rules maps stored in PermissionContextState. Previously-allowed work can re-prompt, or DENY under DONT_ASK. Needs confirmation plus a migration or dual-spelling lookup.
  • [Info] adapter/HarnessAgentTaskStarter.java:464 — a shadowed action is still advertised in availableActions; if a terminal action is the one dropped, the leader stalls. Consider naming dropped pairs in the prompt or refusing the dispatch.
  • [Info] test/HarnessAgentTaskOutcomeTest.java:379 — two more cases worth pinning: outcome-tool shadowing, and a wire name outside [A-Za-z0-9_-].

Notes

  • Static review only; I did not compile or run the module in this sweep (the local mirror could not be synced this cycle), so the CI run remains the authority on build/test status.
  • The roleInstructions string concatenation reads a little awkwardly after interpolation (e.g. " also" + " completes ..."), but the output is correct and the new assertFalse(...contains("run.node.complete")) assertions are a nice guard against the dotted spelling leaking back into prompts.

Automated review by github-manager-bot

String submitResultModelName =
AgentTaskCollaborationTool.toModelName(
AgentTaskCollaborationTool.WIRE_TASK_SUBMIT_RESULT);
if (!runtimeAgent.getToolkit().getToolNames().contains(submitResultModelName)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the '.' -> '_' normalization, this guard assumes "a tool named task_submit_result already in the toolkit must be our outcome tool". That assumption is not safe — a foreign tool can occupy the same model name, which is exactly the situation the new collidingModelNameKeepsFirstRegisteredTool test demonstrates for task_get.

If it happens here, AgentTaskOutcomeTool is never registered, no business outcome can ever be submitted, and runToOutcome keeps emitting its "The turn ended without a business outcome" nudge until the turn budget is exhausted. The collaboration-tool loop below at least logs a warning on collision; this path is silent.

Suggest matching on identity rather than name, e.g. getTool(submitResultModelName) instanceof AgentTaskOutcomeTool, and either replacing the shadowed tool or failing the dispatch loudly — this is the one model-facing name the completion protocol cannot do without.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 095675e.

ensureOutcomeToolRegistered now identity-checks getTool(task_submit_result) instanceof AgentTaskOutcomeTool (outcome tool converted to a ToolBase so the check is real, not ReflectiveFunctionTool-wrapped). A foreign occupant is removed with a warning and replaced; idempotent re-dispatch stays quiet. Covered by foreignOutcomeToolIsReplacedSoBusinessOutcomeStillWorks.

if (wireName == null) {
return null;
}
return wireName.replace('.', '_');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only . is normalized, while the class comment states the constraint as function.name =~ ^[a-zA-Z0-9_-]+$. Wire names containing any other character outside that class (:, /, whitespace, non-ASCII) pass through unchanged, so the "OpenAI-safe" property holds only for the dotted-names case the control plane happens to publish today; length is also unbounded.

Core already has a sanitizer for the same rule (SubAgentTool.resolveToolName, documented as ^[a-zA-Z0-9_-]{1,64}$ with a deterministic-hash fallback). Reusing it here — or asserting the regex and falling back to a stable suffixed name — would make the mapping total instead of case-specific, and would also remove the collision surface this PR had to add a warning for.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 095675e.

toModelName is now a total sanitizer matching ^[a-zA-Z0-9_-]{1,64}$: dotted control-plane names keep the fast '.'→'_' path; anything else (e.g. mcp:server.tool) is sanitized with a deterministic 8-hex hash suffix (same intent as core SubAgentTool.sanitizeName, without the call_ prefix). Class javadoc updated. Covered by toModelNameSanitizesWireNamesOutsideOpenAiCharset.

public final class AgentTaskOutcomeTool {
@Tool(
name = "task.submit_result",
name = "task_submit_result",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renaming the model-facing tool from task.submit_result to task_submit_result also invalidates persisted permission rules: PermissionEngine.rulesFor(...) does an exact key lookup on tool.getName() against the allow/deny/ask tables, and those tables are persisted in PermissionContextState (allow_rules / deny_rules / ask_rules). A session or stored config that granted ALLOW for the dotted spelling stops matching, so previously-approved work re-prompts — or, under DONT_ASK, falls through to DENY.

Could you confirm nothing deployed references the dotted name (session stores, seeded rules, prompt templates), and either migrate those entries or accept both spellings during rule lookup? Worth one line in the docs/release note either way.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 095675e.

PermissionEngine.rulesFor now also accepts legacy dotted spellings whose '.'→'_' form equals the live tool name (and the reverse when the live name is still dotted). So persisted task.submit_result / issue.comment.add allow/deny/ask keys keep matching task_submit_result / issue_comment_add. Brief note on AgentTaskOutcomeTool + Compatibility section in the PR body. Covered by PermissionEngineTest$DualSpellingLookup.

new AgentTaskCollaborationTool(collaboration, definition));
} else {
LOG.warning(
"Skipping collaboration tool registration due to model-name collision:"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging collisions is the right call. One gap remains: the dropped action is still advertised by availableActions, so the only signal the model gets is the Actual tool names available to this agent: line at the end of the prompt. If a terminal action (run.node.complete / run.node.fail) is the one shadowed, a leader can never converge and the run just stalls.

Consider naming the dropped wire/model pairs explicitly in the prompt, or refusing the dispatch when a terminal action cannot be exposed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 095675e.

Dropped colliding wire→model pairs are now listed in the kickoff prompt (Dropped collaboration tools due to model-name collision (not callable): …). If a terminal action (run.node.complete / run.node.fail) cannot be exposed, dispatch fails loudly with IllegalStateException. Covered by terminalActionCollisionRefusesDispatch.

}

@Test
void collidingModelNameKeepsFirstRegisteredTool() throws Exception {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good coverage of the rebase follow-ups — the local-collision case and the idempotent re-dispatch case are both pinned here. Two more tests would lock down the risky paths flagged inline above:

  • outcome-tool shadowing: pre-register a foreign tool under task_submit_result and assert the dispatch either still captures a business outcome or fails loudly;
  • a wire name outside [A-Za-z0-9_-] (e.g. mcp:server.tool), so the normalization contract is specified rather than implied by the dotted-name fixture.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 095675e.

Added:

  • foreignOutcomeToolIsReplacedSoBusinessOutcomeStillWorks — foreign task_submit_result is replaced; business outcome still finishes.
  • toModelNameSanitizesWireNamesOutsideOpenAiCharset — mcp:server.tool normalization contract.
  • terminalActionCollisionRefusesDispatch — terminal shadow refuses dispatch.

aistio module: 81 tests, 0 failures.

Replace outcome-tool name guard with AgentTaskOutcomeTool identity
check (replace foreign occupants), make toModelName a total OpenAI-safe
sanitizer with hash fallback, accept dotted/underscore permission rule
aliases, refuse terminal model-name collisions and list dropped tools in
the kickoff prompt, and pin the risky paths with tests.
@tengjiaozhai

Copy link
Copy Markdown
Contributor Author

oss-maintainer review follow-up (095675e)

@oss-maintainer all open review findings from 2026-09-21 are addressed on fix/aistio-collaboration-tool-model-names — please re-review.

Finding Fix
Critical — silent skip when foreign tool occupies task_submit_result Identity check instanceof AgentTaskOutcomeTool; replace foreign occupant with warning
Warning — toModelName only mapped . Total sanitizer ^[a-zA-Z0-9_-]{1,64}$ + deterministic hash fallback
Warning — permission rules break on rename PermissionEngine.rulesFor dual-spelling (dotted ↔ underscore via '.'→'_' alias)
Info — dropped actions still advertised Dropped pairs named in kickoff prompt; terminal collision refuses dispatch
Info — missing tests Outcome shadowing, mcp:server.tool normalization, terminal collision

Verify: mvn -pl agentscope-extensions/agentscope-extensions-aistio -am spotless:apply && spotless:check && test → aistio 81 tests, 0 failures.

@oss-maintainer oss-maintainer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Re-review after 095675ea ("address oss-maintainer dual-name review findings"). All five findings from the previous round are genuinely resolved, and each fix is pinned by a test rather than asserted in prose:

  • outcome tool registration is now identity-checked (ensureOutcomeToolRegistered → getTool(...) instanceof AgentTaskOutcomeTool, foreign occupant removed and replaced) — foreignOutcomeToolIsReplacedSoBusinessOutcomeStillWorks
  • toModelName is now a total sanitizer matching core SubAgentTool semantics (charset + 64-char cap + deterministic hash suffix) — toModelNameSanitizesWireNamesOutsideOpenAiCharset
  • the terminal-action collision no longer stalls the leader silently: it is reported into the kickoff prompt and terminalActionCollisionRefusesDispatch refuses dispatch
  • task.complete / task.fail exclusion moved onto the shared WIRE_* constants
  • the task.submit_result rename keeps persisted permission rules matching via PermissionEngine#rulesFor

CI is green on the new head on both runners, CLA signed, MERGEABLE/CLEAN. Not approving this round for one reason only: the compatibility fix for an agentscope-extensions-aistio tool name was implemented in agentscope-core's permission engine, as an unconditional bidirectional name-alias merge for every tool. That is a much wider blast radius than the bug it fixes, and it is the one part of the PR where I would want either a narrower mechanism or an explicit maintainer sign-off. Everything else here I would take as-is.

Findings

  • [Warning] core/.../PermissionEngine.java:329 — the alias merge is bidirectional and unconditional, so a rule authored for tool a.b can now match a different tool registered as a_b (allow and deny directions).
  • [Warning] core/.../PermissionEngine.java:310 — rulesFor degrades from an O(1) map hit to a full table scan with replace() allocations on every permission check for any tool without an exact-key rule set; it runs three times per tool invocation.
  • [Info] aistio/.../AgentTaskOutcomeTool.java:45 — LEGACY_DOTTED_NAME is unused, so it cannot keep the two spellings in sync with the engine.
  • [Info] aistio/.../AgentTaskOutcomeTool.java:130 — a [null] in pending_task_ids escapes as an NPE from Stream.toList() instead of a tool error.
  • [Info] aistio/.../HarnessAgentTaskStarter.java:574 — interpolation-driven string splitting; output is correct, readability is not.

Notes

  • Static review only this cycle; CI on head 095675ea (ubuntu + windows build, license, module sync, codecov/patch) is all green and is the authority on build/test status.
  • If the alias behaviour is deliberate for 2.0 adoption, consider a core-level follow-up issue so the trade-off is documented rather than implicit in a helper's comment.

Automated review by github-manager-bot

toolName.equals(key.replace('.', '_'))
|| (toolName.contains(".") && key.equals(toolName.replace('.', '_')));
if (!alias) {
continue;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Warning] The alias merge is bidirectional and unconditional, so it is no longer true that a rule only ever constrains the tool it was authored for.

toolName.equals(key.replace('.', '_')) matches any table key whose dots were turned into underscores. A DENY stored for a real dotted tool a.b now also denies a completely unrelated tool that happens to be registered as a_b (and the reverse branch does the symmetric thing for ALLOW). Both directions are privilege-relevant in a permission engine, and this is agentscope-core — it applies to every tool in every deployment, not just the aistio adapter.

Could the match be narrowed to names the tool itself declares as legacy spellings?

// ToolBase: default no-alias, AgentTaskOutcomeTool overrides to return LEGACY_DOTTED_NAME
List<String> aliases = tool.nameAliases();
for (String alias : aliases) {
    merged.addAll(table.getOrDefault(alias, List.of()));
}

At minimum, requiring toolName to contain no dot for the forward branch (i.e. only an underscore-normalised name may pull in a dotted key) would stop the reverse direction from granting an ALLOW authored for a first-class underscore tool to a dotted twin. Either way, the javadoc above ("the legacy dotted spelling") should describe what the code actually does today.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2ab8a85.

Replaced the unconditional bidirectional string-shape merge with opt-in ToolBase.nameAliases() (default empty). PermissionEngine.rulesFor now only merges keys the tool itself declares — AgentTaskOutcomeTool returns LEGACY_DOTTED_NAME (task.submit_result) only. Unrelated a.b / a_b pairs no longer cross-match in either direction (underscore ALLOW no longer grants a dotted twin). Javadoc updated to describe the opt-in behavior.

* task.submit_result} still matches live {@code task_submit_result}).
*/
private static List<PermissionRule> rulesFor(
Map<String, List<PermissionRule>> table, String toolName) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Warning] rulesFor was an O(1) map hit; the scan runs whenever the exact-key lookup did not already return the whole rule set — which includes the common case of a tool that simply has no rules. Each call is now O(alias keys x rules) with two replace() allocations per entry, and checkAllowRules / checkDenyRules / checkAskRules (lines 266/279/292) all go through it, so every single tool invocation in the ReAct loop pays it three times against a rule table that grows with every learned suggestion.

Cheap fixes: skip the loop when the table has no dotted keys (tables.keySet().stream().noneMatch(k -> k.contains(".")) precomputed at construction), or cache the expanded alias list per toolName in the engine.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2ab8a85 (same change as the alias narrowing).

With opt-in aliases there is no table scan: tools with empty nameAliases() take a single Map.get (O(1)). Aliased tools do O(|aliases|) keyed lookups — no replace() / full entry walk on every checkAllow/Deny/Ask.

public static final String MODEL_NAME = "task_submit_result";

/** Legacy dotted spelling previously used as {@code @Tool(name=...)} / permission keys. */
public static final String LEGACY_DOTTED_NAME = "task.submit_result";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Info] LEGACY_DOTTED_NAME is declared (and advertised in the class javadoc as the reason persisted keys keep matching), but nothing references it — the PermissionEngine lookup is purely string-shape-based, so the constant cannot actually keep the two spellings in sync and will drift silently.

Either feed it into the lookup (the per-tool alias hook suggested in the PermissionEngine comment), assert on it in PermissionEngineTest, or drop it and let the javadoc refer to AgentTaskCollaborationTool.toModelName(...) as the single source of truth.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2ab8a85.

AgentTaskOutcomeTool.nameAliases() returns List.of(LEGACY_DOTTED_NAME), so the constant is the source of truth for permission dual-lookup. Asserted in outcomeToolDeclaresLegacyDottedNameAsPermissionAlias.

return null;
}
if (value instanceof List<?> list) {
return list.stream().map(v -> v == null ? null : String.valueOf(v)).toList();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Info] The v == null branch here makes the method throw instead of degrading: Stream.toList() rejects null elements, so a model emitting "pending_task_ids": [null] produces an NPE out of callAsync rather than an IllegalArgumentException/ToolResultBlock.error like every other malformed input on this path.

return list.stream().filter(Objects::nonNull).map(String::valueOf).toList();

(List.of(String.valueOf(value)) at line 132 is fine — that overload prints the literal "null".)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2ab8a85.

stringListArg now does list.stream().filter(Objects::nonNull).map(String::valueOf).toList(), so [null] no longer NPEs out of Stream.toList().

+ " call only when every child Issue and worker node has"
+ " converged. Never send those mutations in parallel. "
+ runNodeComplete
+ " also"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Info] The token now reaches the model as "... also" + " completes ...", i.e. the string was split at every interpolation rather than at the existing sentence boundaries. Output is correct, but this block is going to be painful to reword — worth one pass to reflow the concatenation points (or switch to a text block plus formatted(...)) before this grows another instruction.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2ab8a85.

Reflowed roleInstructions at sentence boundaries and switched the team-role branches to .formatted(...) so interpolations sit at readable phrase breaks (no more " also" + " completes" mid-token splits).

Replace unconditional bidirectional dotted/underscore alias merge in
PermissionEngine with ToolBase.nameAliases(); only AgentTaskOutcomeTool
declares LEGACY_DOTTED_NAME. Restores O(1) lookup for tools without
aliases, filters null pending_task_ids, and reflows roleInstructions.
@tengjiaozhai

Copy link
Copy Markdown
Contributor Author

@oss-maintainer polished the five findings from #3205 (review) in 2ab8a85 — requesting re-review.

PermissionEngine: opt-in aliases (blocking)

Unconditional bidirectional '.'↔'_' merge is gone. Lookup is now opt-in via ToolBase.nameAliases() (default empty / no alias). Only AgentTaskOutcomeTool overrides to declare LEGACY_DOTTED_NAME (task.submit_result). Unrelated a.b vs a_b no longer cross-match either direction. Tools without aliases stay O(1) (Map.get only).

Other findings

  1. O(1) — no table scan when aliases empty; aliased tools do keyed lookups only.
  2. LEGACY_DOTTED_NAME — wired through nameAliases(); asserted in outcomeToolDeclaresLegacyDottedNameAsPermissionAlias.
  3. pending_task_ids — nulls filtered before Stream.toList().
  4. roleInstructions — reflowed at sentence boundaries with .formatted(...).

Tests

  • agentscope-core: 2409 tests, 0 failures (9 skipped); PermissionEngineTest$DualSpellingLookup now 3 cases (declared alias works; dotted/underscore twins without aliases do not cross-match).
  • agentscope-extensions-aistio: 82 tests, 0 failures (+1 alias assertion).
mvn -pl agentscope-core,agentscope-extensions/agentscope-extensions-aistio -am spotless:apply
mvn -pl agentscope-core,agentscope-extensions/agentscope-extensions-aistio spotless:check
mvn -pl agentscope-core,agentscope-extensions/agentscope-extensions-aistio -am test

Thread replies: alias merge · O(1) · LEGACY wiring · null filter · roleInstructions

@oss-maintainer oss-maintainer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Re-review of 2ab8a85f (follow-up to the findings raised on 095675ea). The coupling heuristic in PermissionEngine.rulesFor — which previously auto-matched any dotted/underscore string reshaping — has been replaced with an explicit opt-in ToolBase.nameAliases()" hook: rules now merge only names a tool itself declares (AgentTaskOutcomeTool→ legacytask.submit_result), the no-alias path stays a single O(1) map hit, and the new negative tests (a.bvsa_b` twins not cross-matching in either direction) pin the security property exactly. This fully resolves the dual-spelling concern; the aistio-side rename and the rest of the PR are unchanged from the earlier review. Approving — merge state CLEAN, thanks for the quick turnaround.


Automated review by github-manager-bot

@oss-maintainer oss-maintainer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Re-review response to @tengjiaozhai's two re-review requests (2026-09-22). I re-read 2ab8a85 against the five findings from the 2026-09-21 review, and all five are genuinely addressed — no new issues found in the changed surface, so this is an approval.

Verified in the diff:

  • Permission aliases are opt-in — ToolBase.nameAliases() defaults to an empty list, and PermissionEngine.rulesFor(...) short-circuits to the exact-key lookup when there are no aliases, so the previous unconditional '.'↔'_' cross-matching (and its per-lookup list building) is gone.
  • LEGACY_DOTTED_NAME is declared only by AgentTaskOutcomeTool, which is the single tool that actually needs persisted-key compatibility.
  • Total model-name sanitizer — ^[a-zA-Z0-9_-]{1,64}$ with a deterministic 8-hex suffix whenever characters were dropped or the length budget was exceeded, so two distinct wire names cannot silently collide onto the same model name.
  • pending_task_ids null-filtering and the roleInstructions reflow are in place.
  • Tests cover outcome shadowing, mcp:server.tool normalization and terminal collision.

Note for maintainers

The PermissionEngine change is in agentscope-core, so it affects every tool name lookup, not just the aistio adapter; the empty-alias fast path keeps that cost at one Map.get, which is the right shape. CLA (license/cla) is green and the branch is mergeable.


Automated review by github-manager-bot

This branch has not been deployed

No deployments
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.

aistio: collaboration tool names use dots and still break OpenAI-compatible models (HTTP 400)

2 participants