Skip to content

fix(engine): resolve @-links per scope, and settle the remaining ADK gaps - #138

Merged
Cidan merged 1 commit into
mainfrom
fix/remaining-adk-gaps
Aug 21, 2026
Merged

fix(engine): resolve @-links per scope, and settle the remaining ADK gaps#138
Cidan merged 1 commit into
mainfrom
fix/remaining-adk-gaps

Conversation

@Cidan

@Cidan Cidan commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Closes the last items from the ADK audit. One real bug, two pieces of dead code that should not be wired, one deferred with a reason, and one item that was my error.

@-links resolve per scope (real bug)

An @-link in ~/.claude/CLAUDE.md was resolved against the project root. Your actual case: it contains @RTK.md, which sits right next to it, and ask looked for <repo>/RTK.md and silently found nothing. It became reachable when #133 started loading the user-global scope.

type ContextScope struct {
	Dir  string   // where instruction files are searched
	Root string   // what this scope's @-links resolve against
}

LoadedContextDoc.Root and Rule.Root carry it, BuildSystemPrompt groups links by root before resolving, and the JIT rule path uses r.Root. Verified against your real ~/.claude:

included_docs block present: true
   <file path="/home/antonio/.claude/CLAUDE.md">
   <file path="/home/antonio/.claude/RTK.md">
RTK.md content reached the prompt: true

functioncallmodifier removed, not enabled

It injects synthetic arguments into tool declarations at request time. ask needed that for the required description phrase — which is now a static field on every native tool's params struct, so there is nothing left to inject. It had been registered with Predicate: func(string) bool { return false } since #132.

Bridge tools still get description added to their input schema in pkg/tools/bridge.go, because their input types come from the MCP handler cores. That's a one-time build at construction, not per-request surgery.

agenttool rejected, four dead builders deleted

agent_tool.go:142 builds its own runner with a hardcoded config:

runner.New(runner.Config{
	AppName:         t.agent.Name(),
	Agent:           t.agent,
	SessionService:  sessionService,
	ArtifactService: artifact.InMemoryService(),
	MemoryService:   memory.InMemoryService(),
})

No PluginConfig, so a subagent would lose retryandreflect. MemoryService is in-memory, so it would lose ask's memory. And it produces one tool per agent, replacing task(agent: "foo") with a tool named foo.

The task tool's nested engine.Run goes through RunnerBuilder — ask's plugins, memory service, and file session service — and keeps the background-job path and the subagent UI events. BuildResearchSubagent / BuildNamedSubagent / BuildResearchAgentTool / BuildNamedAgentTool had no production callers and are deleted.

toolconfirmation deferred

It emits adk_request_confirmation, pauses the run, and resumes on a function response. ask has no suspend/resume path for a chat turn, so adopting it means building one — a design change, not a bug fix. Approval stays ToolEnv.ApprovalDenied, which blocks on the modal and returns the denial inline.

IsConfirmationCall / UnwrapConfirmationCall stay wired in the event loops, because an MCP server can declare confirmation on its own tools and those branches render the inner intent correctly if it ever fires. FormatConfirmationResponse had no callers at all and is deleted.

Correction: the deferred registry was not a bug

I listed "empty deferred registry on the engine path" as a gap. That was wrong. The task tool already threads the parent session's registry:

deferredFn := func() []tools.Tool { … sess.deferredTools() … }
toolsList = tools.CoreTools(subEnv, deferredFn, true)
engine.Run(runCtx, engine.RunOptions{ Tools: toolsList })

engine.Run uses opts.Tools when non-empty, so GetDefaultToolFactory() and its nil registry are never reached. Subagents do see MCP and linear_*. The nil applies only to a pure headless engine.Run with no tools passed, where there genuinely is no registry source — MCP isn't connected and the linear tools live in cmd/ask.

Docs

docs/adk-20-upgrade.md gains a Deliberate departures section, so none of these gets wired later by someone reading an import as an intention.

Tests

  • global-scope @-links resolve under ~/.claude, and do not pick up a same-named file in the project
  • project-scope links still resolve under the project root
  • every coding tool declares description statically — the reason functioncallmodifier is unnecessary
  • DefaultPlugins stays free of the modifier

make test green across all 9 packages.

🤖 Generated with Claude Code

…gaps

Closes the last items from the ADK audit. Two were real bugs, two were
dead code that should not be wired, and one is deferred with a reason.

@-links now resolve against the scope that owns the document, not against
the project root. An @-link in ~/.claude/CLAUDE.md — the real case is
"@RTK.md", which sits next to it — was looked up as <repo>/RTK.md and
silently found nothing. It became reachable when PR #133 started loading
the user-global scope. AgentContextScopes pairs each search directory
with its link root, LoadedContextDoc.Root and Rule.Root carry it, and
BuildSystemPrompt groups links by root before resolving. The JIT rule
path uses r.Root for the same reason.

functioncallmodifier is removed rather than enabled. It injects synthetic
arguments into tool declarations at request time; ask needed that for the
required `description` phrase, which is now a static field on every
native tool's params struct, so there is nothing left to inject. It had
been registered with a predicate that always returned false since PR
#132. Bridge tools still get `description` added to their input schema in
pkg/tools/bridge.go, because their input types come from the MCP handler
cores — a one-time build at construction, not per-request surgery.

agenttool is rejected, and its four dead builders are deleted.
agent_tool.go builds its own runner with a hardcoded config: no
PluginConfig, so a subagent loses retryandreflect, and MemoryService:
memory.InMemoryService(), so it loses ask's memory. It also produces one
tool per agent, replacing task(agent: "foo") with a tool named foo. The
task tool's nested engine.Run goes through RunnerBuilder — ask's plugins,
memory, and file session service — and keeps the background-job path and
the subagent UI events.

toolconfirmation is deferred, not forgotten. It emits an
adk_request_confirmation call, pauses the run, and resumes on a function
response; ask has no suspend/resume path for a chat turn, so adopting it
means building one. Approval stays ToolEnv.ApprovalDenied, blocking on
the modal. IsConfirmationCall / UnwrapConfirmationCall stay wired because
an MCP server can declare confirmation on its own tools;
FormatConfirmationResponse had no callers at all and is deleted.

One item from the audit was my error and needed no change: the deferred
registry is not empty for subagents. The task tool passes the parent
session's deferred tools as opts.Tools, so engine.Run never reaches the
nil registryFunc. It is nil only on a pure headless engine.Run with no
tools supplied, where there genuinely is no registry — MCP is not
connected and the linear tools live in cmd/ask.

docs/adk-20-upgrade.md gains a "Deliberate departures" section so none of
these gets wired later by someone reading an import as an intention.

Tests: global-scope @-links resolve under ~/.claude and do not pick up a
same-named file in the project; project-scope links still resolve under
the project root; every coding tool declares `description` statically,
which is what makes functioncallmodifier unnecessary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Cidan
Cidan force-pushed the fix/remaining-adk-gaps branch from af6db48 to d22469d Compare August 21, 2026 07:31
@Cidan
Cidan merged commit 6724099 into main Aug 21, 2026
@Cidan
Cidan deleted the fix/remaining-adk-gaps branch August 21, 2026 07:32
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.

1 participant