--log_level / --verbose never reach library logs from the agent file: the loader bundles a second copy of @google/adk
- Dominant language
- TypeScript
- Stars
- 1.4k
- Forks
- 205
- Avg merge
- 3d 16h
- Merged PRs (30d)
- 92
Description
### Describe the bug
`adk run` / `adk web` / `adk api_server` set the core log level exactly once, on
the CLI's own copy of `@google/adk`
([`dev/src/cli/cli.ts:372`](https://github.com/google/adk-js/blob/main/dev/src/cli/cli.ts#L372),
and the equivalent call in the `web` / `api_server` actions).
The agent file is then loaded through `AgentFile`, which esbuild-builds it with
`packages: 'bundle'`
([`dev/src/utils/agent_loader.ts:194-224`](https://github.com/google/adk-js/blob/main/dev/src/utils/agent_loader.ts#L194-L224)),
and `bundle` defaults to `true` (`cli.ts:140`, `agent_loader.ts:92-96`). That
inlines a **second copy of `@google/adk`** into the temp module, with its own
`let currentLogger` (`core/src/utils/logger.ts:119`) sitting at the
`SimpleLogger` default of `INFO` (`core/src/utils/logger.ts:35`).
So every library log emitted from inside an agent file — and from anything that
file imports — is filtered at `INFO`, whatever the flag says. The flag raises
the level on the CLI's copy only. The two copies never meet.
This is the deeper gap noted in #658. #658 is necessary but not sufficient: it
makes `--log_level debug` resolve to `DEBUG` instead of `INFO`, and that
corrected level still lands on the wrong copy. The repro below uses `--verbose`,
which already reaches `LogLevel.DEBUG` on `main` today, so the gap is visible
independently of #658 — both flags funnel into the same `setAdkCoreLogLevel`
call.
### To Reproduce
Agent file, logging one line at `debug` and one at `error` as a control:
```ts
// log_repro/agent.ts
import {LlmAgent, getLogger} from '@google/adk';
getLogger().debug('DEBUG-FROM-AGENT-FILE: this should appear with --log_level debug');
getLogger().error('ERROR-FROM-AGENT-FILE: control line, always appears');
export const rootAgent = new LlmAgent({
name: 'log_repro',
model: 'gemini-2.0-flash',
instruction: 'noop',
});
```
```console
$ node dev/dist/esm/cli_entrypoint.js run log_repro/agent.ts --verbose < /dev/null
ERROR: [ADK] 2026-08-12T07:10:21.365Z ERROR-FROM-AGENT-FILE: control line, always appears
Running agent log_repro, type exit to exit.
```
The control line prints, so the logger works and the file really ran. The
`debug` line is gone, with the core log level explicitly at `DEBUG`. The
agent-side copy is at its own untouched `INFO` default, which is exactly why
`error` survives and `debug` does not.
### What isolates it to the bundling
Same host process, `setLogLevel(LogLevel.DEBUG)` called before loading, same
esbuild options as the loader (temp dir, symlinked `node_modules`, same
`external` list). The only variable is whether `@google/adk` is inlined:
```js
setLogLevel(LogLevel.DEBUG);
getLogger().debug('DEBUG-FROM-HOST: CLI-side logger is at DEBUG');
await esbuild.build({
entryPoints: ['agent.ts'], outfile, target: 'node16', platform: 'node',
format: 'esm', packages: 'bundle', bundle: true, plugins: [shimPlugin()],
external: mode === 'external-adk' ? [...EXTERNAL, '@google/adk'] : EXTERNAL,
});
await import(pathToFileURL(outfile).href);
```
```console
# @google/adk bundled in — what the loader does today
DEBUG: [ADK] DEBUG-FROM-HOST: CLI-side logger is at DEBUG
--- mode=bundled ---
ERROR: [ADK] ERROR-FROM-AGENT-FILE: control line, always appears
# @google/adk left external — one shared copy
DEBUG: [ADK] DEBUG-FROM-HOST: CLI-side logger is at DEBUG
--- mode=external-adk ---
DEBUG: [ADK] DEBUG-FROM-AGENT-FILE: this should appear with --log_level debug
ERROR: [ADK] ERROR-FROM-AGENT-FILE: control line, always appears
```
Marking `@google/adk` external is the whole difference between the flag working
and not working.
### Expected behavior
`--log_level debug` (and `--verbose`) applies to library logs emitted from agent
code too — one log level for the process, which is what someone reaching for the
flag is asking for.
### `--bundle false` is not a workaround
The obvious escape hatch fails for an unrelated reason:
```console
$ node dev/dist/esm/cli_entrypoint.js run log_repro/agent.ts --verbose --bundle false
✘ [ERROR] Cannot use "external" without "bundle"
```
`external` is passed unconditionally (`agent_loader.ts:205-223`) while `bundle`
is `this.options.bundle` (`agent_loader.ts:201`), and esbuild rejects `external`
without `bundle`. Since `compile` defaults to `true`, `shouldCompile` is still
true and the build still runs. Happy to split this into its own issue if you'd
rather track it separately — it is a one-line guard, but it means there is
currently no way to opt out of the duplicate copy.
### Scope
- `run`, `web`, and `api_server` share both halves of the wiring
(`setAdkCoreLogLevel` + `getAgentFileOptions`), so all three behave the same.
Verified end-to-end on `run`.
- Not only the level: `setLogger()` mutates the same module-scoped
`currentLogger`, so a custom logger installed by the host is not the one
agent-side library code writes to. Same cause, quieter symptom.
- More generally, any module-scoped state in `core` is duplicated across this
boundary. The logger is just where it is user-visible.
### Possible fixes
Maintainers' call; roughly in order of how much they fix:
1. **Mark `@google/adk` external in the loader's esbuild call** and let it
resolve through the `node_modules` the loader already symlinks into the temp
dir (`agent_loader.ts:192`). One copy, no split state, and the temp bundle
stops carrying the whole SDK (~12 MB unminified in my repro). Needs a check
that nothing downstream relies on that bundle being self-contained — `deploy`
goes through the same loader (`deploy_utils.ts:203`).
2. **Make the logger state process-global** — hang `currentLogger` off
`globalThis[Symbol.for('@google/adk.logger')]` — so duplicate copies
converge. Fixes the class of bug rather than this one flag, and is the safer
option if the bundle must stay self-contained.
3. **Inject a prelude into the bundle** that calls the bundled copy's own
`setLogLevel` with the CLI's level. Narrowest: fixes the level, leaves
`setLogger` and everything else split.
I'd lean on 1, with 2 as a backstop. Happy to send a PR for whichever you prefer.
### Environment
- adk-js `main` @ 8611219, `core`/`dev` 1.6.0
- Node v22.20.0
- macOS 15.7.7
Contributor guide
Assessment
This issue has not been assessed yet.