google / google/adk-js

--log_level / --verbose never reach library logs from the agent file: the loader bundles a second copy of @google/adk

Open
#661 0 comments 0 reactions 1 assignee Claimed by @Varun-S10 View on GitHub
bug
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.