mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-08 16:11:58 -07:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 48f0a83191 | |||
| f5dce650a4 | |||
| 99267012bc | |||
| 53bbc2b339 | |||
| a31412ba83 | |||
| cf4114062b | |||
| eb03bd16b9 | |||
| 9f3a154014 | |||
| 137c4cd59c |
@@ -0,0 +1,529 @@
|
||||
# ADK-TS Alignment Pass
|
||||
|
||||
Every interface in our outline must map cleanly to ADK-TS. This document
|
||||
verifies that mapping field-by-field, identifies gaps, and confirms
|
||||
HITL/plugin/transfer patterns work.
|
||||
|
||||
Source: ADK-TS v0.4.0 at `/Users/adamfweidman/Desktop/adk-int/adk-js/core/src/`
|
||||
|
||||
---
|
||||
|
||||
## 1. AgentDescriptor ↔ ADK Agent Hierarchy
|
||||
|
||||
### Field-by-field mapping
|
||||
|
||||
| AgentDescriptor field | ADK-TS source | Notes |
|
||||
| ---------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `name` | `BaseAgent.name` | Direct. ADK validates it's a valid JS identifier. |
|
||||
| `displayName` | — | ADK doesn't have this. No conflict. |
|
||||
| `description` | `BaseAgent.description` (optional in ADK) | Direct. Used for model routing in AgentTool. |
|
||||
| `executor` | — | New concept. ADK agents are always 'adk'. Adapter sets this. |
|
||||
| `inputSchema` | `LlmAgent.inputSchema` (Zod or JSON Schema) | Direct. ADK's AgentTool uses this for tool parameter generation. |
|
||||
| `outputSchema` | `LlmAgent.outputSchema` (Zod or JSON Schema) | Direct. ADK uses for structured output + AgentTool response. |
|
||||
| `capabilities` | — | New concept. Adapter infers from agent type: LlmAgent gets `['elicitation', 'streaming', 'host_tool_execution']`, LoopAgent gets `['composition']`, etc. |
|
||||
| `ownTools` | `LlmAgent.tools: ToolUnion[]` | Maps via ToolDescriptor adapter. ADK tools have `name`, `description`, `_getDeclaration()` which returns JSON Schema. |
|
||||
| `requiredTools` | — | New concept. ADK agents don't declare required host tools. Adapter can infer from tool references. |
|
||||
| `subAgents` | `BaseAgent.subAgents: BaseAgent[]` | Recursive. Each sub-agent becomes a nested AgentDescriptor. |
|
||||
| `constraints.maxTurns` | `RunConfig.maxLlmCalls` (default 500) | Maps, though semantics differ slightly (LLM calls vs turns). |
|
||||
| `constraints.maxTimeMinutes` | — | ADK doesn't have time limits. No conflict — host enforces. |
|
||||
| `constraints.maxBudgetUsd` | — | ADK doesn't have budget. No conflict — host enforces. |
|
||||
| `metadata` | — | New concept. Adapter can populate from agent registration context. |
|
||||
|
||||
### ADK-specific fields NOT in AgentDescriptor
|
||||
|
||||
| ADK field | Where it lives | Our approach |
|
||||
| ----------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `instruction` / `globalInstruction` | LlmAgent | Executor-internal. Not in descriptor (it's runtime config, not identity). |
|
||||
| `model` | LlmAgent | Goes in ExecutionOptions.model or executor-internal config. |
|
||||
| `generateContentConfig` | LlmAgent | Executor-internal. |
|
||||
| `disallowTransferToParent/Peers` | LlmAgent | Could be `constraints` or `_meta`. Transfer policy is host-enforced. |
|
||||
| `includeContents` | LlmAgent | Executor-internal (context management). |
|
||||
| `outputKey` | LlmAgent | Executor-internal (state management). |
|
||||
| `beforeModelCallback`, etc. | LlmAgent | Executor-internal. These are ADK's callback system — our LifecycleInterceptor is the interface equivalent. |
|
||||
|
||||
### Verdict: CLEAN MAPPING
|
||||
|
||||
AgentDescriptor captures everything needed to describe an ADK agent externally.
|
||||
ADK-specific runtime config (instruction, model, callbacks) stays inside the
|
||||
executor — exactly right for the descriptor/executor separation.
|
||||
|
||||
**Key ADK pattern preserved:** AgentTool wraps an agent as a tool using
|
||||
`inputSchema` for parameters and `description` for the tool description. Our
|
||||
AgentDescriptor has both, so SubagentTool can do the same thing.
|
||||
|
||||
---
|
||||
|
||||
## 2. AgentSession ↔ ADK Runner
|
||||
|
||||
### Method mapping
|
||||
|
||||
| AgentSession method | ADK-TS equivalent | How adapter works |
|
||||
| ----------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `stream(data, options)` | `Runner.runAsync({ userId, sessionId, newMessage, runConfig })` | Adapter creates/loads session, maps data+options → runAsync params, wraps Event generator → AgentEvent generator. Each `stream()` call triggers a new `runAsync()`. |
|
||||
| `update(config)` | No direct equivalent | ADK doesn't support mid-stream config changes. Adapter queues updates for next `runAsync()` call. |
|
||||
| `steer(data)` | No direct equivalent | ADK doesn't support mid-stream intervention. Adapter can queue for next invocation or ignore. |
|
||||
| `abort()` | No direct equivalent | ADK uses `invocationContext.endInvocation = true`. Adapter sets this flag. Could also use AbortController. |
|
||||
|
||||
### ExecutionRequest → Runner.runAsync mapping
|
||||
|
||||
| ExecutionRequest field | ADK mapping |
|
||||
| --------------------------- | ---------------------------------------------------------------- |
|
||||
| `descriptor` | Used to find/create the BaseAgent instance |
|
||||
| `input` | → `newMessage: Content` (converted from ContentPart[] → Content) |
|
||||
| `sessionRef` | → `sessionId` (string) or creates session from SessionSnapshot |
|
||||
| `forkSession` | Adapter clones session before running |
|
||||
| `options.tools` | → merged into agent's `tools` config |
|
||||
| `options.model` | → `LlmAgent.model` override |
|
||||
| `options.hostToolExecution` | → `RunConfig.pauseOnToolCalls: true` |
|
||||
| `options.streaming` | → `RunConfig.streamingMode` |
|
||||
| `options.permissionMode` | → SecurityPlugin config |
|
||||
| `signal` | → wired to `invocationContext.endInvocation` |
|
||||
|
||||
### HITL: How pauseOnToolCalls works end-to-end
|
||||
|
||||
This is the critical path. Here's the full flow:
|
||||
|
||||
```
|
||||
1. LLM returns tool call (FunctionCall in Event)
|
||||
2. ADK checks RunConfig.pauseOnToolCalls === true
|
||||
3. ADK sets invocationContext.endInvocation = true
|
||||
4. ADK yields the Event (with FunctionCall) and stops
|
||||
5. Runner.runAsync() generator completes
|
||||
|
||||
--- OUR INTERFACE BOUNDARY ---
|
||||
|
||||
6. Adapter translates ADK Event → ToolRequestEvent
|
||||
7. Host receives ToolRequestEvent from session.stream() generator
|
||||
8. Host runs policy check (PolicyEvaluator.evaluate())
|
||||
9. Host fires hooks (LifecycleInterceptor.fire('before_tool', ...))
|
||||
10. If policy allows → Host executes tool → gets ToolResultData
|
||||
11. Host calls session.stream({ kind: 'tool_result', ... }) to get next stream
|
||||
|
||||
--- BACK INTO ADK ---
|
||||
|
||||
12. Adapter receives tool result
|
||||
13. Adapter creates FunctionResponse Content
|
||||
14. Adapter calls Runner.runAsync() again with FunctionResponse as newMessage
|
||||
15. ADK loads session (has prior tool call event)
|
||||
16. ADK resumes agent with tool response
|
||||
17. Loop continues from step 1
|
||||
```
|
||||
|
||||
**Why this works:** ADK's `pauseOnToolCalls` was designed exactly for this
|
||||
pattern — external tool execution by a host. The adapter translates between
|
||||
ADK's "end invocation + resume with FunctionResponse" pattern and our
|
||||
"ToolRequestEvent + send(tool_result)" pattern.
|
||||
|
||||
**Key insight:** Each `session.stream()` call triggers a new `Runner.runAsync()`
|
||||
call. This means each ADK "invocation" maps to one `stream()` call. The session
|
||||
persists state across invocations. Mid-stream `update()` and `steer()` calls are
|
||||
queued for the next invocation since ADK doesn't support mid-turn changes.
|
||||
|
||||
### HITL: ToolConfirmation flow
|
||||
|
||||
ADK also has a separate ToolConfirmation pattern (via
|
||||
`context.requestConfirmation()`):
|
||||
|
||||
```
|
||||
1. beforeToolCallback calls context.requestConfirmation({ hint: '...' })
|
||||
2. This sets eventActions.requestedToolConfirmations[functionCallId]
|
||||
3. ADK yields event with requestedToolConfirmations populated
|
||||
4. Runner completes (invocation ends)
|
||||
|
||||
--- OUR INTERFACE BOUNDARY ---
|
||||
|
||||
5. Adapter sees requestedToolConfirmations in event
|
||||
6. Adapter translates → ElicitationRequest { kind: 'tool_confirmation', ... }
|
||||
7. Host renders confirmation UI
|
||||
8. User responds → ElicitationResponse { action: 'accept' | 'decline' }
|
||||
|
||||
--- BACK INTO ADK ---
|
||||
|
||||
9. Adapter receives elicitation response
|
||||
10. If accepted: Adapter creates FunctionResponse with confirmed=true
|
||||
11. Calls Runner.runAsync() with FunctionResponse
|
||||
12. ADK's SecurityPlugin or callback reads confirmation from session
|
||||
13. Tool executes
|
||||
```
|
||||
|
||||
**Maps to our ElicitationRequest:** ADK's `ToolConfirmation.hint` →
|
||||
`ElicitationRequest.message`. ADK's `ToolConfirmation.payload` →
|
||||
`ElicitationRequest.context`. The `kind: 'tool_confirmation'` is the
|
||||
discriminator.
|
||||
|
||||
### HITL: Auth request flow
|
||||
|
||||
```
|
||||
1. Tool or callback calls context.requestCredential(authConfig)
|
||||
2. Sets eventActions.requestedAuthConfigs[functionCallId]
|
||||
3. Event yields, invocation ends
|
||||
|
||||
--- OUR INTERFACE BOUNDARY ---
|
||||
|
||||
4. Adapter sees requestedAuthConfigs
|
||||
5. Translates → ElicitationRequest { kind: 'auth_required', context: authConfig }
|
||||
6. User provides credentials
|
||||
7. ElicitationResponse { action: 'accept', content: { credential: ... } }
|
||||
|
||||
--- BACK INTO ADK ---
|
||||
|
||||
8. Adapter stores credential via CredentialService
|
||||
9. Calls Runner.runAsync() again
|
||||
10. Tool calls context.getAuthResponse() → gets credential
|
||||
```
|
||||
|
||||
**Maps to our ElicitationRequest:** ADK's auth pattern is just another
|
||||
elicitation kind. This validates our generic elicitation design — it handles
|
||||
tool confirmation, auth, and any future interaction type.
|
||||
|
||||
---
|
||||
|
||||
## 3. AgentEvent ↔ ADK Event
|
||||
|
||||
### Event type mapping
|
||||
|
||||
| Our AgentEvent | ADK Event pattern | Adapter translation |
|
||||
| --------------------- | --------------------------------------------------------------------------------- | --------------------------------------------- |
|
||||
| `InitializeEvent` | First event from Runner.runAsync() | Adapter emits on first stream() call |
|
||||
| `SessionUpdateEvent` | `eventActions.stateDelta` | Adapter emits when stateDelta is non-empty |
|
||||
| `MessageEvent` | `event.content` with text Parts | Filter text/thought parts from Content |
|
||||
| `ToolRequestEvent` | `getFunctionCalls(event)` returns FunctionCall[] | Each FunctionCall → one ToolRequestEvent |
|
||||
| `ToolUpdateEvent` | `event.longRunningToolIds` | Adapter emits progress for long-running tools |
|
||||
| `ToolResponseEvent` | `getFunctionResponses(event)` returns FunctionResponse[] | Each FunctionResponse → one ToolResponseEvent |
|
||||
| `ElicitationRequest` | `eventActions.requestedToolConfirmations` or `requestedAuthConfigs` | Map to generic elicitation |
|
||||
| `ElicitationResponse` | User input → FunctionResponse in next runAsync call | Reverse of above |
|
||||
| `UsageEvent` | `event.usageMetadata` (GenerateContentResponseUsageMetadata) | Map token counts |
|
||||
| `ErrorEvent` | `event.errorCode` + `event.errorMessage` | Map error fields |
|
||||
| `stream_end` | `isFinalResponse(event)`, `eventActions.transferToAgent`, `eventActions.escalate` | Derive `stream_end` reason from ADK signals |
|
||||
| `CustomEvent` | `event.customMetadata` | Pass through |
|
||||
|
||||
### ADK EventActions → Our events
|
||||
|
||||
| EventActions field | Our event | Notes |
|
||||
| ---------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| `stateDelta` | SessionUpdate or embedded in other events | Delta state is a core ADK pattern |
|
||||
| `artifactDelta` | `CustomEvent { kind: 'artifact_delta' }` | Artifacts not in our core events |
|
||||
| `transferToAgent` | Tool call (`transfer_to_agent`) + `stream_end` `reason: 'completed'` | Handoff is a tool call. Host intercepts the tool request, mediates the handoff, originating agent completes. |
|
||||
| `escalate` | `stream_end` `reason: 'completed'` with `data: { escalateReason: '...' }` | LoopAgent exit signal. ADK's escalate = "I'm done, pass control back up" |
|
||||
| `requestedToolConfirmations` | `ElicitationRequest { kind: 'tool_confirmation' }` | Per function call ID |
|
||||
| `requestedAuthConfigs` | `ElicitationRequest { kind: 'auth_required' }` | Per function call ID |
|
||||
| `skipSummarization` | `_meta: { skipSummarization: true }` | ADK-specific, goes in metadata |
|
||||
|
||||
### AgentEventBase mapping
|
||||
|
||||
| AgentEventBase field | ADK Event field | Notes |
|
||||
| -------------------- | ---------------------------------------- | ------------------------------------------------- |
|
||||
| `id` | `event.id` | Direct |
|
||||
| `timestamp` | `event.timestamp` (number) | Convert to ISO 8601 string |
|
||||
| `type` | Derived from content analysis | ADK doesn't have event types — adapter classifies |
|
||||
| `agentId` | `event.author` (agent name) or context | **New field** — which agent emitted this event |
|
||||
| `threadId` | `event.branch` (e.g., "agent_1.agent_2") | Direct mapping |
|
||||
| `source` | `event.author` ("user" or agent name) | Direct |
|
||||
| `_meta` | `event.customMetadata` | Direct |
|
||||
|
||||
### Verdict: CLEAN MAPPING
|
||||
|
||||
Every ADK event pattern maps to our event types. The adapter classifies ADK's
|
||||
untyped events into our typed event taxonomy. Key insight: ADK events are richer
|
||||
(they carry EventActions, function calls, auth requests all in one event), so
|
||||
the adapter may fan out one ADK Event into multiple AgentEvents (e.g., one
|
||||
Message + one ToolRequest + one ElicitationRequest). The new `agentId` field
|
||||
maps directly from ADK's `event.author`.
|
||||
|
||||
---
|
||||
|
||||
## 4. ToolContract ↔ ADK Tool System
|
||||
|
||||
### ToolDescriptor ↔ BaseTool
|
||||
|
||||
| ToolDescriptor field | ADK source | Notes |
|
||||
| ------------------------- | ------------------------------------------------------------- | --------------------------------- |
|
||||
| `name` | `BaseTool.name` | Direct |
|
||||
| `displayName` | — | ADK doesn't have this |
|
||||
| `description` | `BaseTool.description` | Direct |
|
||||
| `parametersSchema` | `BaseTool._getDeclaration()` → FunctionDeclaration.parameters | JSON Schema from declaration |
|
||||
| `annotations.readOnly` | Inferred from tool type | FunctionTool with no side effects |
|
||||
| `annotations.longRunning` | `BaseTool.isLongRunning` | Direct |
|
||||
|
||||
### ToolCallRequest ↔ FunctionCall
|
||||
|
||||
| ToolCallRequest | ADK FunctionCall | Notes |
|
||||
| --------------- | ------------------- | ------ |
|
||||
| `requestId` | `functionCall.id` | Direct |
|
||||
| `name` | `functionCall.name` | Direct |
|
||||
| `args` | `functionCall.args` | Direct |
|
||||
|
||||
### ToolResultData ↔ FunctionResponse + tool return
|
||||
|
||||
| ToolResultData | ADK | Notes |
|
||||
| ---------------- | ------------------------------ | ------------------------------------------------ |
|
||||
| `llmContent` | `FunctionResponse.response` | Adapter wraps into ContentPart[] |
|
||||
| `displayContent` | — | ADK doesn't separate display from model content |
|
||||
| `isError` | Error thrown from `runAsync()` | Adapter catches and sets flag |
|
||||
| `tailCalls` | — | ADK doesn't have tail calls (gemini-cli concept) |
|
||||
|
||||
### AgentTool pattern
|
||||
|
||||
ADK's `AgentTool` wraps a `BaseAgent` as a `BaseTool`:
|
||||
|
||||
- Uses `agent.inputSchema` for tool parameters
|
||||
- Uses `agent.description` for tool description
|
||||
- Creates internal Runner with isolated session
|
||||
- Returns agent output as tool result
|
||||
- Merges state deltas back to parent
|
||||
|
||||
**Our equivalent:** `SubagentTool` wraps `AgentDescriptor` as a tool:
|
||||
|
||||
- Uses `descriptor.inputSchema` for tool parameters
|
||||
- Uses `descriptor.description` for tool description
|
||||
- Creates executor via `SessionFactory.create(descriptor, context)`
|
||||
- Returns execution result as tool result
|
||||
|
||||
**Mapping is 1:1.** The only difference is ADK does it with concrete agent
|
||||
instances; we do it with descriptors + factory.
|
||||
|
||||
---
|
||||
|
||||
## 5. LifecycleInterceptor ↔ ADK Plugin System
|
||||
|
||||
### Hook point mapping
|
||||
|
||||
| Our hook point string | ADK Plugin callback | Mapping |
|
||||
| --------------------- | ----------------------- | ------------------------------------------ |
|
||||
| `'before_agent'` | `beforeAgentCallback` | `payload: { agent, context }` |
|
||||
| `'after_agent'` | `afterAgentCallback` | `payload: { agent, context }` |
|
||||
| `'before_model'` | `beforeModelCallback` | `payload: { context, llmRequest }` |
|
||||
| `'after_model'` | `afterModelCallback` | `payload: { context, llmResponse }` |
|
||||
| `'before_tool'` | `beforeToolCallback` | `payload: { tool, args, context }` |
|
||||
| `'after_tool'` | `afterToolCallback` | `payload: { tool, args, context, result }` |
|
||||
| `'on_event'` | `onEventCallback` | `payload: { event }` |
|
||||
| `'on_user_message'` | `onUserMessageCallback` | `payload: { userMessage }` |
|
||||
| `'before_run'` | `beforeRunCallback` | `payload: { context }` |
|
||||
| `'after_run'` | `afterRunCallback` | `payload: { context }` |
|
||||
| `'on_model_error'` | `onModelErrorCallback` | `payload: { request, error }` |
|
||||
| `'on_tool_error'` | `onToolErrorCallback` | `payload: { tool, args, error }` |
|
||||
|
||||
### HookResult ↔ ADK callback return
|
||||
|
||||
| HookResult field | ADK pattern | Notes |
|
||||
| ------------------- | ----------------------------------------------- | ----------------------------------- |
|
||||
| `action: 'proceed'` | Return `undefined` | Plugin returns nothing → continue |
|
||||
| `action: 'block'` | Return `Content` (for agent/model) or throw | Non-undefined return short-circuits |
|
||||
| `modifications` | Return modified `LlmRequest`/`LlmResponse`/args | Plugin returns modified version |
|
||||
|
||||
### ADK's early-exit pattern
|
||||
|
||||
ADK plugins use "first non-undefined return wins":
|
||||
|
||||
- `beforeModelCallback` returns `LlmResponse` → skips LLM call entirely (cache
|
||||
hit)
|
||||
- `beforeToolCallback` returns modified `args` → tool runs with new args
|
||||
- `beforeAgentCallback` returns `Content` → skips agent run entirely
|
||||
|
||||
Our `HookResult.modifications` carries the same data. The `action: 'block'` +
|
||||
return value pattern maps cleanly.
|
||||
|
||||
### gemini-cli hooks NOT in ADK
|
||||
|
||||
| gemini-cli hook | ADK equivalent | Notes |
|
||||
| --------------------- | ------------------------------------ | ------------------------------------------------------------- |
|
||||
| `BeforeToolSelection` | — | ADK doesn't let you modify which tools are available mid-turn |
|
||||
| `Notification` | — | ADK doesn't have notification hooks |
|
||||
| `SessionStart` | `onUserMessageCallback` (first call) | Close enough |
|
||||
| `SessionEnd` | `afterRunCallback` | Close enough |
|
||||
| `PreCompress` | — | ADK doesn't have context compression hooks |
|
||||
|
||||
These gaps are fine — they're gemini-cli-specific hook points. Our generic
|
||||
`fire(hookPoint, payload)` handles them because the hook point is an open
|
||||
string. ADK executors simply don't fire these hook points, and
|
||||
`supportedHookPoints()` reflects that.
|
||||
|
||||
---
|
||||
|
||||
## 6. PolicyEvaluator ↔ ADK SecurityPlugin
|
||||
|
||||
### ADK SecurityPlugin
|
||||
|
||||
```typescript
|
||||
class SecurityPlugin extends BasePlugin {
|
||||
policyEngine: BasePolicyEngine;
|
||||
|
||||
// In beforeToolCallback:
|
||||
async beforeToolCallback({ tool, args, context }) {
|
||||
const outcome = await this.policyEngine.evaluate(tool.name, args);
|
||||
switch (outcome) {
|
||||
case PolicyOutcome.DENY:
|
||||
throw error;
|
||||
case PolicyOutcome.CONFIRM:
|
||||
context.requestConfirmation({ hint });
|
||||
case PolicyOutcome.ALLOW:
|
||||
return undefined; // proceed
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mapping
|
||||
|
||||
| Our PolicyEvaluator | ADK SecurityPlugin | Notes |
|
||||
| ------------------------- | --------------------------------------------------------- | ------------------------------------------ |
|
||||
| `evaluate(request)` | `policyEngine.evaluate(toolName, args)` | ADK is simpler — tool name + args only |
|
||||
| `PolicyDecision.allow` | `PolicyOutcome.ALLOW` | Direct |
|
||||
| `PolicyDecision.deny` | `PolicyOutcome.DENY` | Direct |
|
||||
| `PolicyDecision.ask_user` | `PolicyOutcome.CONFIRM` → `context.requestConfirmation()` | ADK chains to ToolConfirmation |
|
||||
| `getExcluded()` | — | ADK doesn't pre-filter tools |
|
||||
| `request.principal` | — | ADK doesn't track who's calling |
|
||||
| `request.principalPath` | Could use `context.agentName` + branch | For hierarchical policy |
|
||||
| `request.context` | — | Our extension point for host-specific data |
|
||||
|
||||
### How ADK policy maps when host controls execution
|
||||
|
||||
With `pauseOnToolCalls: true`, the flow is:
|
||||
|
||||
1. ADK yields tool call → adapter converts to ToolRequestEvent
|
||||
2. **Host** runs PolicyEvaluator.evaluate() — NOT ADK's SecurityPlugin
|
||||
3. Host decides allow/deny/ask_user
|
||||
4. If allowed, host executes tool and sends result via `session.stream()`
|
||||
|
||||
This means **ADK's SecurityPlugin is bypassed when the host controls tool
|
||||
execution** — which is correct! The host's PolicyEvaluator is the authority.
|
||||
ADK's SecurityPlugin only matters when ADK executes tools internally
|
||||
(`pauseOnToolCalls: false`).
|
||||
|
||||
---
|
||||
|
||||
## 7. SessionContract ↔ ADK Session
|
||||
|
||||
### Session mapping
|
||||
|
||||
| Our SessionHandle | ADK Session | Notes |
|
||||
| ----------------- | ---------------------------------------- | ----------------------------------------- |
|
||||
| `id` | `Session.id` | Direct |
|
||||
| `agentName` | `Session.appName` | ADK uses appName, not agent name |
|
||||
| `events` | `Session.events: Event[]` | Direct (but ADK Events → our AgentEvents) |
|
||||
| `state` | `Session.state: Record<string, unknown>` | Direct |
|
||||
| `lastUpdateTime` | `Session.lastUpdateTime` | Direct |
|
||||
|
||||
### SessionProvider ↔ BaseSessionService
|
||||
|
||||
| Our SessionProvider | ADK BaseSessionService | Notes |
|
||||
| ----------------------------- | ----------------------------------------------- | -------------------------- |
|
||||
| `create(agentName, metadata)` | `createSession({ appName, userId })` | ADK requires userId |
|
||||
| `load(sessionId)` | `getSession({ appName, userId, sessionId })` | ADK requires all three IDs |
|
||||
| `list(agentName)` | `listSessions({ appName, userId })` | ADK scopes by userId |
|
||||
| `delete(sessionId)` | `deleteSession({ appName, userId, sessionId })` | Same pattern |
|
||||
|
||||
### Gap: ADK requires userId
|
||||
|
||||
ADK sessions are scoped by `(appName, userId, sessionId)`. Our interface uses
|
||||
just `sessionId`. The adapter can embed userId in the session metadata or derive
|
||||
it from HostContext.
|
||||
|
||||
### State prefixes (ADK-specific)
|
||||
|
||||
ADK uses prefixed state keys:
|
||||
|
||||
- `app:` — app-scoped, persisted
|
||||
- `user:` — user-scoped, persisted
|
||||
- `temp:` — temporary, stripped before persistence
|
||||
|
||||
Our `SessionHandle.state` is a flat `Record<string, unknown>`. The adapter
|
||||
preserves prefixes as-is — they're just string keys. No conflict.
|
||||
|
||||
---
|
||||
|
||||
## 8. ContentPart ↔ ADK Content/Part
|
||||
|
||||
### ADK uses Google GenAI types
|
||||
|
||||
ADK's `Content` and `Part` come from `@google/genai`:
|
||||
|
||||
```typescript
|
||||
interface Content {
|
||||
role?: string; // 'user' | 'model'
|
||||
parts: Part[];
|
||||
}
|
||||
|
||||
type Part = TextPart | InlineDataPart | FunctionCallPart | FunctionResponsePart | ...
|
||||
```
|
||||
|
||||
### Mapping
|
||||
|
||||
| Our ContentPart | ADK/GenAI Part | Notes |
|
||||
| --------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------ |
|
||||
| `{ type: 'text', text }` | `{ text: string }` | Direct |
|
||||
| `{ type: 'thought', thought }` | `{ thought: true, text: string }` | ADK uses `thought` boolean flag on TextPart |
|
||||
| `{ type: 'media', mimeType, data }` | `{ inlineData: { mimeType, data } }` | Restructure |
|
||||
| `{ type: 'reference', text, uri }` | `{ fileData: { fileUri, mimeType } }` | Map fileData → reference |
|
||||
| `{ type: 'refusal', text }` | — | Not in ADK/GenAI. Adapter would map from finishReason. |
|
||||
| `{ type: 'function_call', name, args, id }` | `{ functionCall: { name, args, id } }` | Unwrap |
|
||||
| `{ type: 'function_response', name, response, id }` | `{ functionResponse: { name, response, id } }` | Unwrap |
|
||||
|
||||
### Verdict: CLEAN MAPPING
|
||||
|
||||
The adapter converts between our flat discriminated union and ADK's nested Part
|
||||
structure. No information loss in either direction.
|
||||
|
||||
---
|
||||
|
||||
## 9. Composition ↔ ADK Agent Patterns
|
||||
|
||||
| Our CompositionConfig.pattern | ADK Agent type | Notes |
|
||||
| ----------------------------- | -------------------------------------- | ------------------------------------------------ |
|
||||
| `'hierarchical'` | Any agent with `subAgents` | Default — parent calls sub-agents as tools |
|
||||
| `'sequential'` | `SequentialAgent` | Runs children in order |
|
||||
| `'parallel'` | `ParallelAgent` | Runs children concurrently, branch isolation |
|
||||
| `'loop'` | `LoopAgent` | Repeats children until escalate or maxIterations |
|
||||
| `'transfer'` | LlmAgent with `transfer_to_agent` tool | Peer-to-peer handoff |
|
||||
|
||||
### Branch isolation
|
||||
|
||||
ADK's `ParallelAgent` gives each child an isolated `branch` context:
|
||||
|
||||
- Children don't see peer events
|
||||
- Each gets unique branch path: `"parent.child_0"`, `"parent.child_1"`
|
||||
- Results merged after all complete
|
||||
|
||||
Maps to our `threadId` — each parallel branch gets a unique threadId. Events
|
||||
from different branches are interleaved by the host.
|
||||
|
||||
---
|
||||
|
||||
## 10. Summary: Gaps and Resolutions
|
||||
|
||||
### No gaps blocking ADK integration:
|
||||
|
||||
| Concern | Status | Resolution |
|
||||
| ----------------------- | --------- | ------------------------------------------------------------------------- |
|
||||
| pauseOnToolCalls HITL | **Works** | Adapter maps to stream() cycle (§2) |
|
||||
| ToolConfirmation | **Works** | Maps to ElicitationRequest (§2) |
|
||||
| Auth requests | **Works** | Maps to ElicitationRequest (§2) |
|
||||
| Plugin hooks (12 types) | **Works** | Maps to LifecycleInterceptor.fire() (§5) |
|
||||
| Agent transfers | **Works** | Tool call (`transfer_to_agent`) + `stream_end` `reason: 'completed'` (§3) |
|
||||
| State delta pattern | **Works** | SessionUpdateEvent or \_meta (§3) |
|
||||
| Branch isolation | **Works** | threadId mapping (§9) |
|
||||
| AgentTool pattern | **Works** | SubagentTool with descriptor + factory (§4) |
|
||||
| Session management | **Works** | Adapter maps userId into session (§7) |
|
||||
|
||||
### Minor adapter complexity:
|
||||
|
||||
1. **Event fan-out:** One ADK Event may become multiple AgentEvents (message +
|
||||
tool call + elicitation). Adapter logic needed but straightforward.
|
||||
2. **userId scoping:** ADK sessions require userId; our interface doesn't.
|
||||
Adapter derives from HostContext.
|
||||
3. **Timestamp format:** ADK uses `number` (epoch ms); we use ISO 8601 string.
|
||||
Simple conversion.
|
||||
4. **Content structure:** ADK uses nested Part types; we use flat discriminated
|
||||
union. Adapter converts bidirectionally.
|
||||
|
||||
### ADK features our interface supports that gemini-cli doesn't have yet:
|
||||
|
||||
- `LoopAgent` / `ParallelAgent` / `SequentialAgent` composition → our
|
||||
CompositionConfig
|
||||
- `eventActions.stateDelta` → our SessionUpdateEvent
|
||||
- `eventActions.transferToAgent` → tool call (`transfer_to_agent`) +
|
||||
`stream_end` `reason: 'completed'`
|
||||
- `eventActions.escalate` → `stream_end` `reason: 'completed'` with
|
||||
`data: { escalateReason }`
|
||||
- Long-running tools → our ToolUpdateEvent
|
||||
- Auth credential flow → our ElicitationRequest with kind: 'auth_required'
|
||||
@@ -0,0 +1,274 @@
|
||||
# ADK-TS (Agent Development Kit - TypeScript) Architecture Notes
|
||||
|
||||
## Package: `@google/adk` v0.4.0
|
||||
|
||||
**Location:** `/Users/adamfweidman/Desktop/adk-int/adk-js/core/`
|
||||
|
||||
## Agent Hierarchy
|
||||
|
||||
```
|
||||
BaseAgent (abstract)
|
||||
├── LlmAgent - Model-driven agent with tools (the main one)
|
||||
├── LoopAgent - Runs sub-agents in a loop (maxIterations, escalate to exit)
|
||||
├── ParallelAgent - Runs sub-agents concurrently (isolated branches)
|
||||
└── SequentialAgent - Runs sub-agents sequentially
|
||||
```
|
||||
|
||||
### BaseAgent Config
|
||||
|
||||
- `name: string` - Unique identifier (must be valid JS identifier)
|
||||
- `description?: string` - One-line capability for model routing
|
||||
- `parentAgent?: BaseAgent` - Parent in agent tree
|
||||
- `subAgents?: BaseAgent[]` - Child agents
|
||||
- `beforeAgentCallback / afterAgentCallback` - Pre/post execution hooks
|
||||
|
||||
### LlmAgent Config (extends BaseAgent)
|
||||
|
||||
- `model?: string | BaseLlm` - LLM to use
|
||||
- `instruction?: string | InstructionProvider` - Agent-specific instructions
|
||||
- `globalInstruction?: string | InstructionProvider` - Tree-wide (root only)
|
||||
- `tools?: ToolUnion[]` - Available tools
|
||||
- `generateContentConfig?: GenerateContentConfig` - LLM params
|
||||
- `disallowTransferToParent / disallowTransferToPeers` - Transfer controls
|
||||
- `includeContents?: 'default' | 'none'` - Context history inclusion
|
||||
- `inputSchema / outputSchema` - Validation schemas
|
||||
- `outputKey?: string` - Session state key for output storage
|
||||
- `beforeModelCallback / afterModelCallback` - LLM hooks
|
||||
- `beforeToolCallback / afterToolCallback` - Tool hooks
|
||||
- `requestProcessors / responseProcessors` - LLM request/response processors
|
||||
- `codeExecutor?: BaseCodeExecutor`
|
||||
|
||||
## Event System
|
||||
|
||||
### Event Interface
|
||||
|
||||
```typescript
|
||||
interface Event extends LlmResponse {
|
||||
id: string;
|
||||
invocationId: string;
|
||||
author?: string; // "user" or agent name
|
||||
actions: EventActions; // State/artifact/auth/transfer operations
|
||||
longRunningToolIds?: string[];
|
||||
branch?: string; // Hierarchical agent path
|
||||
timestamp: number;
|
||||
content?: Content;
|
||||
partial?: boolean; // Streaming indicator
|
||||
}
|
||||
```
|
||||
|
||||
### EventActions
|
||||
|
||||
```typescript
|
||||
interface EventActions {
|
||||
skipSummarization?: boolean;
|
||||
stateDelta: Record<string, unknown>;
|
||||
artifactDelta: Record<string, number>;
|
||||
transferToAgent?: string;
|
||||
escalate?: boolean;
|
||||
requestedAuthConfigs: Record<string, AuthConfig>;
|
||||
requestedToolConfirmations: Record<string, ToolConfirmation>;
|
||||
}
|
||||
```
|
||||
|
||||
### Structured Events (utility layer)
|
||||
|
||||
Converts raw Event to discriminated union:
|
||||
|
||||
```
|
||||
EventType: THOUGHT | CONTENT | TOOL_CALL | TOOL_RESULT | CALL_CODE |
|
||||
CODE_RESULT | ERROR | ACTIVITY | TOOL_CONFIRMATION | FINISHED
|
||||
```
|
||||
|
||||
## Tool System
|
||||
|
||||
### BaseTool (abstract)
|
||||
|
||||
- `name, description, isLongRunning`
|
||||
- `_getDeclaration(): FunctionDeclaration` - OpenAPI schema for LLM
|
||||
- `runAsync(request): Promise<unknown>` - Execute tool
|
||||
- `processLlmRequest(request): Promise<void>` - Preprocessing
|
||||
|
||||
### Concrete Tool Types
|
||||
|
||||
1. **FunctionTool** - Generic typed tools (Zod schema support)
|
||||
2. **AgentTool** - Wrap agents as tools (for hierarchical composition)
|
||||
3. **MCPTool** - Model Context Protocol server tools
|
||||
4. **GoogleSearchTool** - Built-in web search
|
||||
5. **ExitLoopTool** - Signal loop exit
|
||||
6. **LongRunningFunctionTool** - Async long-running operations
|
||||
|
||||
### BaseToolset
|
||||
|
||||
- Filter tools by predicate or string list
|
||||
- `getTools(context)`, `close()`, `isToolSelected()`
|
||||
- **MCPToolset** - Toolset for MCP server connections
|
||||
|
||||
## Session Management
|
||||
|
||||
### Session Interface
|
||||
|
||||
```typescript
|
||||
interface Session {
|
||||
id: string;
|
||||
appName: string;
|
||||
userId: string;
|
||||
state: Record<string, unknown>; // Mutable key-value store
|
||||
events: Event[]; // Complete conversation history
|
||||
lastUpdateTime: number;
|
||||
}
|
||||
```
|
||||
|
||||
### Session Services
|
||||
|
||||
- `BaseSessionService` (abstract) - createSession, getSession, listSessions,
|
||||
deleteSession, appendEvent
|
||||
- `InMemorySessionService` - In-process storage
|
||||
- `DatabaseSessionService` - Mikro-ORM backed (SQL)
|
||||
|
||||
### State Management
|
||||
|
||||
- `State` class wraps base state + delta
|
||||
- `get()` returns from delta if present, else base
|
||||
- `set()` updates delta only
|
||||
- `hasDelta()` checks if changes made
|
||||
|
||||
## Human-in-the-Loop (HITL)
|
||||
|
||||
### Tool Confirmation
|
||||
|
||||
```typescript
|
||||
class ToolConfirmation {
|
||||
hint?: string; // Guidance for user
|
||||
confirmed: boolean; // User approval
|
||||
payload?: unknown; // Additional context
|
||||
}
|
||||
```
|
||||
|
||||
### Security Plugin
|
||||
|
||||
- `beforeToolCallback` - Evaluates policy before tool execution
|
||||
- `BasePolicyEngine` interface with `evaluate()` method
|
||||
- `PolicyOutcome`: DENY | CONFIRM | ALLOW
|
||||
|
||||
### Auth Requests
|
||||
|
||||
- `context.requestCredential(authConfig)` - Request auth from user
|
||||
- `context.getAuthResponse(authConfig)` - Check for auth response
|
||||
- Sets `eventActions.requestedAuthConfigs[functionCallId]`
|
||||
|
||||
## Multi-Agent Patterns
|
||||
|
||||
### Agent Transfer
|
||||
|
||||
- LlmAgent injects `transfer_to_agent(agentName)` tool
|
||||
- Sets `eventActions.transferToAgent = targetAgentName`
|
||||
- Runner resolves target and continues
|
||||
- Can transfer to: sub-agents, parent (if not disabled), peers (if not disabled)
|
||||
|
||||
### Parallel Agent
|
||||
|
||||
- Runs all subAgents concurrently
|
||||
- Isolates each via `branch` context
|
||||
- Sub-agents don't see peer history
|
||||
- Merges event streams with fair ordering
|
||||
|
||||
### Loop Agent
|
||||
|
||||
- Repeatedly runs subAgents
|
||||
- `maxIterations` caps loop count
|
||||
- Exits on `event.actions.escalate === true`
|
||||
|
||||
## Plugin System
|
||||
|
||||
### BasePlugin Lifecycle Hooks (14 hooks!)
|
||||
|
||||
- `onUserMessageCallback` - Preprocess user messages
|
||||
- `beforeRunCallback` - Before agent run (can short-circuit)
|
||||
- `onEventCallback` - Per-event (can modify events)
|
||||
- `afterRunCallback` - Final cleanup
|
||||
- `beforeAgentCallback / afterAgentCallback` - Agent lifecycle
|
||||
- `beforeModelCallback / afterModelCallback` - LLM lifecycle
|
||||
- `onModelErrorCallback` - Model error handling
|
||||
- `beforeToolCallback / afterToolCallback` - Tool lifecycle
|
||||
- `onToolErrorCallback` - Tool error handling
|
||||
|
||||
### Built-in Plugins
|
||||
|
||||
- **LoggingPlugin** - Debug logging
|
||||
- **SecurityPlugin** - Policy enforcement + tool confirmation
|
||||
- **PluginManager** - Plugin orchestration
|
||||
|
||||
## Runner
|
||||
|
||||
### Runner Config
|
||||
|
||||
```typescript
|
||||
interface RunnerConfig {
|
||||
appName: string;
|
||||
agent: BaseAgent; // Root agent
|
||||
plugins?: BasePlugin[];
|
||||
artifactService?: BaseArtifactService;
|
||||
sessionService: BaseSessionService; // Required
|
||||
memoryService?: BaseMemoryService;
|
||||
credentialService?: BaseCredentialService;
|
||||
}
|
||||
```
|
||||
|
||||
### RunConfig (per-run options)
|
||||
|
||||
```typescript
|
||||
interface RunConfig {
|
||||
speechConfig?: SpeechConfig;
|
||||
responseModalities?: Modality[];
|
||||
maxLlmCalls?: number; // Default 500
|
||||
pauseOnToolCalls?: boolean; // Client-side tool execution
|
||||
streamingMode?: StreamingMode; // NONE | SSE | BIDI
|
||||
// ... audio/live configs
|
||||
}
|
||||
```
|
||||
|
||||
### Execution Pipeline
|
||||
|
||||
1. Load or create session
|
||||
2. Create InvocationContext
|
||||
3. Run pluginManager.runOnUserMessageCallback()
|
||||
4. Append user message to session
|
||||
5. Run agent.runAsync(invocationContext) → yields events
|
||||
6. For each non-partial event: append to session
|
||||
7. Run pluginManager.runOnEventCallback()
|
||||
8. Run pluginManager.runAfterRunCallback()
|
||||
|
||||
## Model Layer
|
||||
|
||||
### BaseLlm (abstract)
|
||||
|
||||
- `generateContentAsync(llmRequest, stream?): AsyncGenerator<LlmResponse>`
|
||||
- `connect(llmRequest): Promise<BaseLlmConnection>` - For live/streaming
|
||||
|
||||
### Implementations
|
||||
|
||||
- `Gemini` - Google Gemini API
|
||||
- `ApigeeLlm` - Apigee-wrapped models
|
||||
- `LLMRegistry` - Static registry for model lookup
|
||||
|
||||
## Service Adapters (all abstract base + implementations)
|
||||
|
||||
| Service | Implementations |
|
||||
| --------------------- | ------------------------------ |
|
||||
| BaseSessionService | InMemory, Database (Mikro-ORM) |
|
||||
| BaseArtifactService | InMemory, File, GCS |
|
||||
| BaseMemoryService | InMemory |
|
||||
| BaseCredentialService | InMemory |
|
||||
| BaseCodeExecutor | BuiltIn |
|
||||
|
||||
## Design Patterns
|
||||
|
||||
1. **Symbol-based type guards** - Every class uses `Symbol.for()` + `isXxx()`
|
||||
2. **Abstract base classes** - Service interfaces via abstract classes
|
||||
3. **Async generators** - All agent execution yields events
|
||||
4. **Context objects** - Rich context passed to callbacks/tools
|
||||
5. **Delta state** - Session state + event action deltas
|
||||
6. **Plugin middleware** - 14 hooks at multiple execution points
|
||||
7. **Tree-based hierarchy** - Parent-child agents with root traversal
|
||||
8. **Branch isolation** - Parallel agents use branch paths
|
||||
9. **Callback chains** - Multiple callbacks per stage with early termination
|
||||
@@ -0,0 +1,587 @@
|
||||
# Cross-SDK Comparison: Events, Agents, and Interface Superset
|
||||
|
||||
## 1. AgentEvents: Our Outline vs Michael's
|
||||
|
||||
Our outline and Michael's `Gemini CLI Agents.txt` are **nearly identical** in
|
||||
event taxonomy. The only difference is we added a `stream_end` event type:
|
||||
|
||||
| # | Michael's Events | Our Outline | Delta |
|
||||
| --- | ---------------------- | --------------------- | ------------------------------------------------------------------------------- |
|
||||
| 1 | `initialize` | `InitializeEvent` | Same |
|
||||
| 2 | `session_update` | `SessionUpdateEvent` | Same |
|
||||
| 3 | `message` | `MessageEvent` | Same — streaming handled by AsyncGenerator |
|
||||
| 4 | `tool_request` | `ToolRequestEvent` | Same |
|
||||
| 5 | `tool_update` | `ToolUpdateEvent` | Same |
|
||||
| 6 | `tool_response` | `ToolResponseEvent` | Same |
|
||||
| 7 | `elicitation_request` | `ElicitationRequest` | Same |
|
||||
| 8 | `elicitation_response` | `ElicitationResponse` | Same |
|
||||
| 9 | `usage` | `UsageEvent` | Same |
|
||||
| 10 | `error` | `ErrorEvent` | Same |
|
||||
| 11 | `custom` | `CustomEvent` | Same |
|
||||
| 12 | — | **StreamEnd** | **Added**: completed, failed, aborted, max_turns, max_budget, max_time, refusal |
|
||||
|
||||
### Minor structural differences:
|
||||
|
||||
| Aspect | Michael | Our Outline |
|
||||
| ---------------------- | --------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| **Base type** | `AgentEventCommon` with `type: string` (fully open) | `AgentEventBase` with `type: AgentEventType` (`'known' \| (string & {})`) |
|
||||
| **Agent ID** | — | `agentId` on event base (which agent emitted this event) |
|
||||
| **Event map** | Generic `interface AgentEvents` + mapped type | Same — adopted Michael's pattern for declaration merging extensibility |
|
||||
| **ContentPart.\_meta** | Required (`_meta: Record<string, unknown>`) | Optional (`_meta?: Record<string, unknown>`) |
|
||||
| **ErrorData.status** | Google RPC codes (`'RESOURCE_EXHAUSTED' \| '...'`) | Open string (per our generic philosophy) |
|
||||
| **Message.role** | `'user' \| 'agent' \| 'developer'` | Same |
|
||||
| **Stream end** | Only `initialize` | `stream_end` with `reason` field + open `data` bag |
|
||||
| **Handoff** | Not covered | Tool call (`transfer_to_agent`) — no dedicated event |
|
||||
| **Pausing** | Implicit (elicitation/tool events) | Same — no explicit pause/resume events |
|
||||
|
||||
### Design decisions adopted from Michael
|
||||
|
||||
1. **`interface AgentEvents` + mapped type** — Michael's pattern enables
|
||||
declaration merging, letting any module add new event types without modifying
|
||||
the base definition. Strictly better than an explicit union type.
|
||||
2. **`_meta` on ContentPart** — More extensible. We adopted it (as optional).
|
||||
3. **Implicit pausing** — No separate pause/resume events. When the agent emits
|
||||
an `elicitation_request` or `tool_request`, the stream naturally pauses. The
|
||||
host calls `stream()` to resume.
|
||||
|
||||
---
|
||||
|
||||
## 2. Claude Agent SDK — Key Interfaces
|
||||
|
||||
Source: `@anthropic-ai/claude-agent-sdk`
|
||||
|
||||
### Agent Execution Model
|
||||
|
||||
```typescript
|
||||
// Entry point — not an interface, a function
|
||||
function query({
|
||||
prompt: string | AsyncIterable<SDKUserMessage>,
|
||||
options?: Options
|
||||
}): Query // extends AsyncGenerator<SDKMessage, void>
|
||||
```
|
||||
|
||||
### Message Types (Event Stream)
|
||||
|
||||
```typescript
|
||||
type SDKMessage =
|
||||
| SystemMessage // subtype: "init" | "compact_boundary"
|
||||
| AssistantMessage // Claude's response with tool calls
|
||||
| UserMessage // Tool results fed back
|
||||
| StreamEvent // Raw API stream events (opt-in)
|
||||
| ResultMessage // Final: success | error_max_turns | error_max_budget_usd | error_during_execution
|
||||
| CompactBoundaryMessage; // Context compaction marker
|
||||
```
|
||||
|
||||
### Tool Approval (HITL)
|
||||
|
||||
```typescript
|
||||
canUseTool: async (toolName: string, input: Record<string, any>) =>
|
||||
Promise<
|
||||
| { behavior: 'allow'; updatedInput: Record<string, any> }
|
||||
| { behavior: 'deny'; message: string }
|
||||
>;
|
||||
```
|
||||
|
||||
### Subagent Definition
|
||||
|
||||
```typescript
|
||||
interface AgentDefinition {
|
||||
description: string; // When to invoke
|
||||
prompt: string; // System prompt
|
||||
tools?: string[]; // Available tools (defaults to all)
|
||||
model?: 'sonnet' | 'opus' | 'haiku' | 'inherit';
|
||||
}
|
||||
```
|
||||
|
||||
### Session Management
|
||||
|
||||
```typescript
|
||||
interface Options {
|
||||
continue?: boolean; // Resume most recent session
|
||||
resume?: string; // Resume by session ID
|
||||
forkSession?: boolean; // Branch from resume point
|
||||
persistSession?: boolean; // Default: true
|
||||
maxTurns?: number;
|
||||
maxBudgetUsd?: number; // Spend limit
|
||||
permissionMode?: 'default' | 'acceptEdits' | 'plan' | 'dontAsk' | 'bypassPermissions';
|
||||
structuredOutput?: { type: "json_schema", ... };
|
||||
}
|
||||
```
|
||||
|
||||
### Result (Termination)
|
||||
|
||||
```typescript
|
||||
interface SDKResultMessage {
|
||||
type: 'result';
|
||||
subtype:
|
||||
| 'success'
|
||||
| 'error_max_turns'
|
||||
| 'error_max_budget_usd'
|
||||
| 'error_during_execution'
|
||||
| 'error_max_structured_output_retries';
|
||||
result?: string;
|
||||
total_cost_usd: number;
|
||||
usage: { input_tokens: number; output_tokens: number };
|
||||
num_turns: number;
|
||||
session_id: string;
|
||||
stop_reason: string | null; // "end_turn", "max_tokens", "refusal"
|
||||
}
|
||||
```
|
||||
|
||||
### V2 Preview (Simpler API)
|
||||
|
||||
```typescript
|
||||
await using session = unstable_v2_createSession({ model: "..." });
|
||||
await session.send("Hello!");
|
||||
for await (const msg of session.stream()) { ... }
|
||||
await session.send("Follow-up");
|
||||
for await (const msg of session.stream()) { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. OpenAI Codex SDK / Responses API — Key Interfaces
|
||||
|
||||
### Codex SDK (TypeScript)
|
||||
|
||||
```typescript
|
||||
// Client
|
||||
const codex = new Codex({ env?, config? });
|
||||
const thread = codex.startThread({ workingDirectory?, skipGitRepoCheck? });
|
||||
const thread = codex.resumeThread(threadId);
|
||||
|
||||
// Execution
|
||||
const turn = await thread.run(prompt: string | InputEntry[], options?);
|
||||
const { events } = await thread.runStreamed(prompt);
|
||||
|
||||
// Streaming
|
||||
for await (const event of events) {
|
||||
switch (event.type) {
|
||||
case "item.completed": // event.item
|
||||
case "turn.completed": // event.usage
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Responses API Streaming Events (53 types)
|
||||
|
||||
Organized hierarchically:
|
||||
|
||||
**Response Lifecycle (7):**
|
||||
|
||||
- `response.queued`, `response.created`, `response.in_progress`
|
||||
- `response.completed`, `response.incomplete`, `response.failed`
|
||||
- `error`
|
||||
|
||||
**Content Streaming (8):**
|
||||
|
||||
- `response.output_item.added`, `response.output_item.done`
|
||||
- `response.content_part.added`, `response.content_part.done`
|
||||
- `response.output_text.delta`, `response.output_text.done`
|
||||
- `response.refusal.delta`, `response.refusal.done`
|
||||
|
||||
**Reasoning (6):**
|
||||
|
||||
- `response.reasoning_text.delta`, `response.reasoning_text.done`
|
||||
- `response.reasoning_summary_part.added`,
|
||||
`response.reasoning_summary_part.done`
|
||||
- `response.reasoning_summary_text.delta`,
|
||||
`response.reasoning_summary_text.done`
|
||||
|
||||
**Function Calls (2):**
|
||||
|
||||
- `response.function_call_arguments.delta`,
|
||||
`response.function_call_arguments.done`
|
||||
|
||||
**MCP (8):**
|
||||
|
||||
- `response.mcp_call_arguments.delta`, `response.mcp_call_arguments.done`
|
||||
- `response.mcp_call.in_progress`, `response.mcp_call.completed`,
|
||||
`response.mcp_call.failed`
|
||||
- `response.mcp_list_tools.in_progress`, `response.mcp_list_tools.completed`,
|
||||
`response.mcp_list_tools.failed`
|
||||
|
||||
**Built-in Tools (15):**
|
||||
|
||||
- File search: `in_progress`, `searching`, `completed`
|
||||
- Web search: `in_progress`, `searching`, `completed`
|
||||
- Code interpreter: `in_progress`, `interpreting`, `code.delta`, `code.done`,
|
||||
`completed`
|
||||
- Image gen: `in_progress`, `generating`, `partial_image`, `completed`
|
||||
|
||||
**Audio (4):**
|
||||
|
||||
- `response.audio.delta`, `response.audio.done`
|
||||
- `response.audio.transcript.delta`, `response.audio.transcript.done`
|
||||
|
||||
**Annotations (1):**
|
||||
|
||||
- `response.output_text.annotation.added`
|
||||
|
||||
### OpenAI Agents SDK (higher-level)
|
||||
|
||||
```python
|
||||
# Python-first, but patterns apply
|
||||
class RunItemStreamEvent:
|
||||
name: Literal[
|
||||
"message_output_created",
|
||||
"handoff_requested",
|
||||
"handoff_occurred",
|
||||
"tool_called",
|
||||
"tool_output",
|
||||
"tool_search_called",
|
||||
"tool_search_output_created",
|
||||
"reasoning_item_created",
|
||||
"mcp_approval_requested",
|
||||
"mcp_approval_response",
|
||||
"mcp_list_tools",
|
||||
]
|
||||
|
||||
class AgentUpdatedStreamEvent:
|
||||
# Fires when current agent changes (handoff)
|
||||
new_agent: Agent
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Superset Analysis — What Changes Our Interfaces?
|
||||
|
||||
### Concepts Present in ALL Systems
|
||||
|
||||
| Concept | gemini-cli | ADK-TS | Claude SDK | Codex/OpenAI | Our Interfaces |
|
||||
| --------------------- | ---------- | ------ | ------------- | -------------- | ----------------------- |
|
||||
| Text streaming | ✅ | ✅ | ✅ | ✅ | ✅ MessageEvent |
|
||||
| Tool request/response | ✅ | ✅ | ✅ | ✅ | ✅ ToolRequest/Response |
|
||||
| Thinking/reasoning | ✅ | ✅ | ✅ (thinking) | ✅ (reasoning) | ✅ ContentPart.thought |
|
||||
| Error events | ✅ | ✅ | ✅ | ✅ | ✅ ErrorEvent |
|
||||
| Token usage | ✅ | ✅ | ✅ | ✅ | ✅ UsageEvent |
|
||||
| Tool progress | ✅ | ✅ | — | ✅ | ✅ ToolUpdateEvent |
|
||||
| Session resume | ✅ | ✅ | ✅ | ✅ | ✅ sessionRef |
|
||||
| Subagents | ✅ | ✅ | ✅ | — | ✅ threadId |
|
||||
| Abort/cancel | ✅ | ✅ | ✅ | ✅ | ✅ abort() |
|
||||
| Metadata escape hatch | — | ✅ | — | — | ✅ \_meta |
|
||||
|
||||
### NEW Concepts From Claude/Codex That We Should Incorporate
|
||||
|
||||
#### 4.1 Structured Stream End Reasons (HIGH PRIORITY)
|
||||
|
||||
**What:** Claude SDK has typed termination:
|
||||
`success | error_max_turns | error_max_budget_usd | error_during_execution`.
|
||||
OpenAI has `completed | incomplete | failed`.
|
||||
|
||||
**Why it matters:** We need a `stream_end` event that captures why the stream
|
||||
ended — the one signal not covered by other event types.
|
||||
|
||||
**Final design — `stream_end` with `reason` + open `data` bag:**
|
||||
|
||||
```typescript
|
||||
type StreamEndReason =
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'aborted'
|
||||
| 'max_turns'
|
||||
| 'max_budget'
|
||||
| 'max_time'
|
||||
| 'refusal'
|
||||
| (string & {});
|
||||
|
||||
interface StreamEnd {
|
||||
reason: StreamEndReason;
|
||||
data?: Record<string, unknown>; // { result?, cost?, usage?, numTurns?, error?, ... }
|
||||
}
|
||||
```
|
||||
|
||||
**Design rationale:**
|
||||
|
||||
- Start is covered by `initialize`. Pausing is implicit (elicitation/tool
|
||||
request events). Handoff is a tool call (`transfer_to_agent`).
|
||||
- End-of-stream details go in `data` as an open bag, not fixed fields.
|
||||
|
||||
#### 4.2 Budget Constraints (MEDIUM PRIORITY)
|
||||
|
||||
**What:** Claude SDK has `maxBudgetUsd`. Neither gemini-cli nor ADK has this
|
||||
today.
|
||||
|
||||
**Why it matters:** Cost control is critical for production deployments.
|
||||
|
||||
**Proposed change to AgentConstraints:**
|
||||
|
||||
```typescript
|
||||
interface AgentConstraints {
|
||||
maxTurns?: number;
|
||||
maxTimeMinutes?: number;
|
||||
maxLlmCalls?: number;
|
||||
maxBudgetUsd?: number; // NEW: from Claude SDK
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.3 Session Forking (MEDIUM PRIORITY)
|
||||
|
||||
**What:** Claude SDK supports `forkSession: boolean` — branch from a resume
|
||||
point to explore alternatives.
|
||||
|
||||
**Why it matters:** Enables "what if" exploration without destroying history.
|
||||
Useful for plan mode.
|
||||
|
||||
**Proposed change to ExecutionRequest:**
|
||||
|
||||
```typescript
|
||||
interface ExecutionRequest {
|
||||
// ... existing fields ...
|
||||
sessionRef?: string | SessionSnapshot;
|
||||
forkSession?: boolean; // NEW: branch from sessionRef instead of continuing
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.4 Permission Modes on Execution (MEDIUM PRIORITY)
|
||||
|
||||
**What:** Claude has 5 permission modes:
|
||||
`default | acceptEdits | plan | dontAsk | bypassPermissions`. gemini-cli has 4
|
||||
approval modes: `default | autoEdit | yolo | plan`.
|
||||
|
||||
**Why it matters:** Both systems have this concept. It should be in
|
||||
ExecutionOptions, not hard-coded.
|
||||
|
||||
**Proposed change to ExecutionOptions:**
|
||||
|
||||
```typescript
|
||||
interface ExecutionOptions {
|
||||
// ... existing fields ...
|
||||
permissionMode?: string; // Open string. Conventions: 'default' | 'auto_edit' | 'autonomous' | 'plan' | string
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.5 Agent Handoff (MEDIUM PRIORITY)
|
||||
|
||||
**What:** OpenAI Agents SDK has explicit `handoff_requested` /
|
||||
`handoff_occurred` events plus `AgentUpdatedStreamEvent`. ADK has
|
||||
`transfer_to_agent` tool + `eventActions.transferToAgent`. Claude SDK has
|
||||
subagent invocation via Agent tool.
|
||||
|
||||
**Why it matters:** When agent A delegates to agent B, the host/UI needs to
|
||||
know.
|
||||
|
||||
**Design decision: Handoff is a tool call, not a separate event type.**
|
||||
|
||||
The agent calls `transfer_to_agent` as a tool (ToolRequest event). The host
|
||||
intercepts this tool call (since host controls tool execution), looks up the
|
||||
target agent, creates a new executor via the factory, and mediates the handoff.
|
||||
The originating agent's stream ends with `stream_end` reason `'completed'`.
|
||||
|
||||
```typescript
|
||||
// 1. Agent emits tool request:
|
||||
{ type: 'tool_request', name: 'transfer_to_agent', args: { target: 'coder', reason: '...' } }
|
||||
|
||||
// 2. Host mediates handoff, originating agent completes:
|
||||
{ type: 'stream_end', reason: 'completed', agentId: 'planner', data: { handoffTarget: 'coder' } }
|
||||
```
|
||||
|
||||
This avoids duplicating routing logic between stream_end events and tool calls.
|
||||
Matches ADK's `transfer_to_agent` tool pattern.
|
||||
|
||||
#### 4.6 Refusal as Distinct Signal (LOW PRIORITY)
|
||||
|
||||
**What:** OpenAI has explicit `response.refusal.delta/done` events. Claude has
|
||||
`stop_reason: "refusal"`.
|
||||
|
||||
**Why it matters:** Model refusals are operationally important (safety, policy).
|
||||
|
||||
**Proposed:** No new event type. Handle via `MessageEvent` with a `refusal`
|
||||
content part type, or via `ErrorEvent` with specific error code. ContentPart can
|
||||
be extended:
|
||||
|
||||
```typescript
|
||||
| { type: 'refusal'; text: string }
|
||||
```
|
||||
|
||||
#### 4.7 Content Annotations (LOW PRIORITY)
|
||||
|
||||
**What:** OpenAI has `response.output_text.annotation.added` for citations, file
|
||||
paths.
|
||||
|
||||
**Why it matters:** Citations and source attribution are increasingly important.
|
||||
|
||||
**Proposed:** Michael's `reference` ContentPart already covers this. No change
|
||||
needed — `reference` with `uri` and `text` handles citations.
|
||||
|
||||
#### 4.8 Context Compaction Events (LOW PRIORITY)
|
||||
|
||||
**What:** Claude SDK has `CompactBoundaryMessage` marking when context was
|
||||
compressed.
|
||||
|
||||
**Why it matters:** For long sessions, knowing when context was compressed helps
|
||||
with debugging and UI.
|
||||
|
||||
**Proposed:** `CustomEvent` with `kind: 'compact_boundary'`. No new event type
|
||||
needed.
|
||||
|
||||
#### 4.9 Structured Output Schema (ALREADY COVERED)
|
||||
|
||||
**What:** Both Claude (`structuredOutput`) and OpenAI support JSON Schema output
|
||||
constraints.
|
||||
|
||||
**Status:** Already covered by `AgentDescriptor.outputSchema: JsonSchema`. No
|
||||
change needed.
|
||||
|
||||
### Concepts We DON'T Need to Adopt
|
||||
|
||||
| Concept | Why Skip |
|
||||
| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| OpenAI's 53 granular streaming events | Too coupled to Responses API internals. Our `ToolUpdateEvent` + `MessageEvent` via AsyncGenerator abstracts over this. |
|
||||
| OpenAI's per-tool-type events (file_search, web_search, code_interpreter) | Tool-specific progress belongs in `ToolUpdateEvent.data`, not in the event taxonomy. |
|
||||
| Audio/Image streaming events | Handle via `ToolUpdateEvent` with media ContentParts. When needed, add as ContentPart types, not event types. |
|
||||
| Claude's raw `StreamEvent` wrapper | Implementation detail of the Claude API client. Our adapters consume these internally. |
|
||||
| MCP-specific events (mcp_call, mcp_list_tools) | MCP tools are just tools. Use generic `ToolRequestEvent/ToolResponseEvent`. MCP approval is an `ElicitationRequest`. |
|
||||
|
||||
---
|
||||
|
||||
## 5. Updated Event Type Comparison (Full Superset)
|
||||
|
||||
| # | Event Type | Michael | Our Outline | Claude SDK | OpenAI | Verdict |
|
||||
| --- | -------------------- | ------- | ----------- | ----------------------------- | --------------------------------- | ---------------------------------------------- |
|
||||
| 1 | Initialize | ✅ | ✅ | SystemMessage(init) | — | **Keep** |
|
||||
| 2 | Session Update | ✅ | ✅ | — | — | **Keep** |
|
||||
| 3 | Message | ✅ | ✅ | AssistantMessage | output_text.delta/done | **Keep** |
|
||||
| 4 | Tool Request | ✅ | ✅ | AssistantMessage.tool_use | function_call_arguments | **Keep** |
|
||||
| 5 | Tool Update | ✅ | ✅ | — | per-tool progress events | **Keep** |
|
||||
| 6 | Tool Response | ✅ | ✅ | UserMessage | — | **Keep** |
|
||||
| 7 | Elicitation Request | ✅ | ✅ | canUseTool callback | mcp_approval_requested | **Keep** |
|
||||
| 8 | Elicitation Response | ✅ | ✅ | canUseTool return | mcp_approval_response | **Keep** |
|
||||
| 9 | Usage | ✅ | ✅ | ResultMessage.usage | response.completed | **Keep** |
|
||||
| 10 | Error | ✅ | ✅ | ResultMessage(error\_\*) | response.failed | **Keep** |
|
||||
| 11 | Custom | ✅ | ✅ | — | — | **Keep** |
|
||||
| 12 | StreamEnd | — | ✅ | ResultMessage + SystemMessage | response.created/completed/failed | **Keep — `stream_end` with `reason` + `data`** |
|
||||
|
||||
**Result: Our 12 event types are the right abstraction level.** Claude and
|
||||
OpenAI validate every category. The granularity differences (OpenAI's 53 vs
|
||||
our 12) are implementation details that adapters handle internally. `stream_end`
|
||||
uses a single `reason` field with an open `data` bag. Handoff is a tool call.
|
||||
Pausing is implicit.
|
||||
|
||||
---
|
||||
|
||||
## 6. Updated ContentPart Types (Superset)
|
||||
|
||||
```typescript
|
||||
type ContentPart = (
|
||||
| { type: 'text'; text: string }
|
||||
| { type: 'thought'; thought: string; thoughtSignature?: string }
|
||||
| { type: 'media'; data?: string; uri?: string; mimeType?: string }
|
||||
| {
|
||||
type: 'reference';
|
||||
text: string;
|
||||
data?: string;
|
||||
uri?: string;
|
||||
mimeType?: string;
|
||||
}
|
||||
| { type: 'refusal'; text: string } // NEW: from OpenAI
|
||||
) &
|
||||
// Future: type: string for unknown types from new SDKs
|
||||
{ _meta?: Record<string, unknown> };
|
||||
```
|
||||
|
||||
Adding `refusal` as a ContentPart type (rather than a new event) keeps the event
|
||||
taxonomy stable while supporting model refusals from both Claude and OpenAI.
|
||||
|
||||
---
|
||||
|
||||
## 7. Key Architectural Patterns Across SDKs
|
||||
|
||||
### Pattern: Execution Entry Points
|
||||
|
||||
| SDK | Entry Point | Multi-turn Pattern |
|
||||
| ----------- | ------------------------------------------------------------------------- | ----------------------------------------------- |
|
||||
| Michael | `agent.send(trajectory, data)` / `session.send()` + `session.update()` | Same method / three-method session |
|
||||
| Our Outline | `session.stream(data)` + `session.update(config)` + `session.steer(data)` | Four-method session (stream/update/steer/abort) |
|
||||
| Claude SDK | `query({ prompt, options })` | New `query()` call with `resume: sessionId` |
|
||||
| Claude V2 | `session.send()` + `session.stream()` | Separate send/stream |
|
||||
| Codex SDK | `thread.run(prompt)` / `thread.runStreamed(prompt)` | Same thread object |
|
||||
|
||||
**Observation:** Claude V2 and Codex both use a stateful session/thread object
|
||||
with send+stream. Michael uses a single `send()` method. Our `stream()` method
|
||||
is the unified version — the first call starts, subsequent calls continue (like
|
||||
ADK's `runAsync()`).
|
||||
|
||||
### Pattern: Tool Approval
|
||||
|
||||
| SDK | Pattern | Sync/Async |
|
||||
| ----------- | -------------------------------------------------------- | ------------------------ |
|
||||
| gemini-cli | PolicyEngine + ConfirmationBus | Async (message bus) |
|
||||
| ADK-TS | SecurityPlugin.policyCheck() | Async (plugin callback) |
|
||||
| Claude SDK | `canUseTool()` callback | Async (callback) |
|
||||
| OpenAI | `mcp_approval_requested` event | Event-based |
|
||||
| Our Outline | `ElicitationRequest` event + `PolicyEvaluator` interface | Both (event + interface) |
|
||||
|
||||
**Observation:** Our approach covers both patterns — the `ElicitationRequest`
|
||||
event for event-based approval (like OpenAI), and the `PolicyEvaluator`
|
||||
interface for synchronous policy checks (like gemini-cli/ADK/Claude). This is
|
||||
the right superset.
|
||||
|
||||
### Pattern: Subagent Definition
|
||||
|
||||
| SDK | Pattern | Key Fields |
|
||||
| ------------- | -------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| gemini-cli | `AgentDefinition` (local/remote) | name, description, kind, tools, model |
|
||||
| ADK-TS | `BaseAgentConfig` | name, description, subAgents, tools |
|
||||
| Claude SDK | `AgentDefinition` | description, prompt, tools, model |
|
||||
| OpenAI Agents | `Agent` class | name, instructions, tools, handoffs, model |
|
||||
| Our Outline | `AgentDescriptor` | name, description, executor, inputSchema, capabilities, ownTools, requiredTools, subAgents |
|
||||
|
||||
**Observation:** Our `AgentDescriptor` is the most complete. Claude's `prompt`
|
||||
field and OpenAI's `instructions` are executor-level concerns (system prompt),
|
||||
not descriptor-level. The descriptor declares identity; the executor uses the
|
||||
prompt. This separation is correct.
|
||||
|
||||
One gap: **handoffs**. OpenAI Agents has an explicit `handoffs` field listing
|
||||
which agents can be delegated to. Our `subAgents` field serves the same purpose
|
||||
but the naming implies hierarchy rather than peer delegation. Consider whether
|
||||
`subAgents` should be renamed to `delegateAgents` or kept as-is with
|
||||
documentation clarifying it covers both hierarchical and peer delegation.
|
||||
|
||||
---
|
||||
|
||||
## 8. Concrete Changes to outline.md
|
||||
|
||||
Based on this analysis, the following changes should be made:
|
||||
|
||||
### Applied (validated by multiple SDKs):
|
||||
|
||||
1. ✅ **`type: AgentEventType`** with known values + `(string & {})`
|
||||
(autocomplete + extensibility)
|
||||
2. ✅ **`interface AgentEvents` + mapped type** (adopted from Michael for
|
||||
declaration merging)
|
||||
3. ✅ **`agentId` on event base** (which agent emitted this event)
|
||||
4. ✅ **`_meta` on ContentPart** (aligned with Michael)
|
||||
5. ✅ **`stream_end` event** — signals why the stream ended, with `reason`
|
||||
field + open `data` bag
|
||||
6. ✅ **Handoff as tool call** — `transfer_to_agent` tool, not a separate event
|
||||
7. ✅ **`maxBudgetUsd` in AgentConstraints** (Claude SDK, increasingly standard)
|
||||
8. ✅ **`refusal` ContentPart type** (both Claude and OpenAI surface refusals)
|
||||
9. ✅ **`forkSession` in ExecutionRequest** (Claude SDK, valuable for
|
||||
exploration)
|
||||
10. ✅ **`permissionMode` in ExecutionOptions** (both gemini-cli and Claude SDK)
|
||||
11. ✅ **`cost` field on Usage** (Claude SDK tracks total_cost_usd)
|
||||
|
||||
### Correctly abstracted (no change needed):
|
||||
|
||||
- Event taxonomy (12 types) — validated as right abstraction level
|
||||
- `AgentDescriptor` shape — most complete across all SDKs
|
||||
- `AgentSession.stream/update/steer/abort` — covers all SDK patterns
|
||||
- ToolUpdate — correctly abstracts over OpenAI's 15+ tool-specific progress
|
||||
events
|
||||
- `ElicitationRequest/Response` — covers both callback and event patterns
|
||||
- `ContentPart` types — text/thought/media/reference/refusal
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- [Claude Agent SDK TypeScript Reference](https://platform.claude.com/docs/en/agent-sdk/typescript)
|
||||
- [Claude Agent SDK Streaming](https://platform.claude.com/docs/en/agent-sdk/streaming-output)
|
||||
- [Claude Agent SDK Sessions](https://platform.claude.com/docs/en/agent-sdk/sessions)
|
||||
- [Claude Agent SDK Subagents](https://platform.claude.com/docs/en/agent-sdk/subagents)
|
||||
- [OpenAI Codex SDK TypeScript](https://github.com/openai/codex/tree/main/sdk/typescript)
|
||||
- [OpenAI Codex SDK Docs](https://developers.openai.com/codex/sdk/)
|
||||
- [OpenAI Responses API Streaming Events](https://developers.openai.com/api/reference/resources/responses/streaming-events/)
|
||||
- [OpenAI Agents SDK Streaming](https://openai.github.io/openai-agents-python/streaming/)
|
||||
- [Responses API Streaming Guide (Community)](https://community.openai.com/t/responses-api-streaming-the-simple-guide-to-events/1363122)
|
||||
@@ -0,0 +1,259 @@
|
||||
# Gemini CLI Architecture Notes
|
||||
|
||||
## Project Structure
|
||||
|
||||
**Monorepo packages:**
|
||||
|
||||
- `packages/core/` - Main execution engine (the big one)
|
||||
- `packages/cli/` - CLI frontend
|
||||
- `packages/sdk/` - SDK for extensions
|
||||
- `packages/a2a-server/` - Agent-to-agent server
|
||||
- `packages/devtools/` - Dev utilities
|
||||
- `packages/vscode-ide-companion/` - VS Code extension
|
||||
|
||||
## Core Execution Loop
|
||||
|
||||
### GeminiClient (`core/src/core/client.ts` ~38KB)
|
||||
|
||||
- **Primary orchestrator** for user interactions
|
||||
- Manages session lifecycle, message routing, model selection
|
||||
- Coordinates hooks, context management, error recovery
|
||||
- Enforces `MAX_TURNS = 100` per session
|
||||
- Tracks `currentSequenceModel` for multi-turn stickiness
|
||||
- Handles history compression when context grows
|
||||
|
||||
### GeminiChat (`core/src/core/geminiChat.ts` ~34KB)
|
||||
|
||||
- Bidirectional LLM communication
|
||||
- Maintains `history[]` alternating user/model turns
|
||||
- Retry logic: max 2 attempts, 500ms delay for invalid responses
|
||||
- Fires `BeforeModel` and `AfterModel` hooks
|
||||
- Integrates ChatRecordingService for persistence
|
||||
|
||||
### Scheduler (`core/src/scheduler/scheduler.ts` ~23KB)
|
||||
|
||||
- **Three-phase event-driven**: Ingestion → Processing → Completion
|
||||
- Tool call state machine:
|
||||
`Validating → AwaitingApproval → Scheduled → Executing → Terminal`
|
||||
- Terminal states: `Success`, `Error`, `Cancelled`
|
||||
- Parallel execution for read-only and agent-type tools
|
||||
- Yields to event loop for user approval
|
||||
- Publishes state changes via MessageBus
|
||||
|
||||
### CoreToolScheduler (`core/src/core/coreToolScheduler.ts` ~38KB)
|
||||
|
||||
- Sequential, queue-based tool processing
|
||||
- Validates policy via PolicyEngine
|
||||
- Confirmation handling via ToolModificationHandler (editor integration)
|
||||
- Uses MessageBus for async confirmation responses
|
||||
|
||||
## Tool System
|
||||
|
||||
### DeclarativeTool Pattern
|
||||
|
||||
- **Separation of concerns**: build() → validate → createInvocation() →
|
||||
execute()
|
||||
- `ToolBuilder` defines metadata (name, displayName, description, kind) + schema
|
||||
via `getSchema()`
|
||||
- `ToolInvocation` has: `getDescription()`, `toolLocations()`,
|
||||
`shouldConfirmExecute()`, `execute()`
|
||||
- `ToolResult` contains: `llmContent` (for LLM), `returnDisplay` (for UI), error
|
||||
details, tail calls
|
||||
|
||||
### BaseToolInvocation
|
||||
|
||||
- Abstract base with MessageBus integration for policy/confirmation
|
||||
- Three decision paths: ALLOW, DENY, ASK_USER via `getMessageBusDecision()`
|
||||
|
||||
### ToolRegistry (`core/src/tools/tool-registry.ts`)
|
||||
|
||||
- Registers tools via `registerTool()`
|
||||
- MCP tools with fully qualified names: `mcp_serverName_toolName`
|
||||
- Priority sorting: built-in → discovered → MCP (by server name)
|
||||
- Filters by active status based on configuration
|
||||
|
||||
### Confirmation System
|
||||
|
||||
- `ToolCallConfirmationDetails` union: edit, execute, MCP, info, ask_user,
|
||||
exit_plan_mode
|
||||
- `ToolConfirmationOutcome` enum: ProceedOnce, ProceedAlways, etc.
|
||||
- Async confirmation via MessageBus pub/sub
|
||||
|
||||
## Hooks System
|
||||
|
||||
### Hook Types (11 hook points)
|
||||
|
||||
| Hook | Trigger | Key Capability |
|
||||
| --------------------- | ----------------------- | --------------------------------- |
|
||||
| `BeforeTool` | Before tool execution | Modify tool_input |
|
||||
| `AfterTool` | After tool completion | Context injection, tail calls |
|
||||
| `BeforeAgent` | Before agent prompt | Additional context |
|
||||
| `AfterAgent` | After agent response | Clear context flag |
|
||||
| `BeforeModel` | Before LLM request | Modify request or inject response |
|
||||
| `AfterModel` | After LLM response | Modify response |
|
||||
| `BeforeToolSelection` | Before tool selection | Modify toolConfig |
|
||||
| `Notification` | When notifications fire | Suppress/modify message |
|
||||
| `SessionStart` | Session begins | Additional context |
|
||||
| `SessionEnd` | Session terminates | Cleanup |
|
||||
| `PreCompress` | Before compression | Suppress/modify |
|
||||
|
||||
### Hook Output Fields (common to all hooks)
|
||||
|
||||
- `continue` - Whether execution proceeds
|
||||
- `stopReason` - Reason to halt
|
||||
- `suppressOutput` - Hide from user
|
||||
- `systemMessage` - Add to system context
|
||||
- `decision` - ask/block/deny/approve/allow
|
||||
|
||||
### Hook System Components
|
||||
|
||||
- `HookSystem` - Main coordinator
|
||||
- `HookRegistry` - Stores/manages configurations
|
||||
- `HookRunner` - Executes registered hooks
|
||||
- `HookAggregator` - Combines multiple hook results
|
||||
- `HookPlanner` - Determines execution order
|
||||
- `HookEventHandler` - Orchestrates event firing
|
||||
- `HookTranslator` - Converts between formats
|
||||
|
||||
## Policy Engine
|
||||
|
||||
### Rule Structure
|
||||
|
||||
```
|
||||
PolicyRule {
|
||||
toolName: string; // wildcards supported
|
||||
decision: PolicyDecision; // ALLOW | DENY | ASK_USER
|
||||
priority: number;
|
||||
argsPattern?: RegExp; // conditional on args
|
||||
mcpName?: string;
|
||||
source: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Tier Hierarchy (lowest → highest priority)
|
||||
|
||||
1. Default (1) - Core built-in policies
|
||||
2. Extension (2) - Extension contributions
|
||||
3. Workspace (3) - Project-scoped (.gemini/)
|
||||
4. User (4) - User-provided (~/.gemini/)
|
||||
5. Admin (5) - System-level policies
|
||||
|
||||
### Dynamic Rule Priorities (within User Tier)
|
||||
|
||||
- 4.9 - MCP_EXCLUDED (persistent server blocks)
|
||||
- 4.4 - EXCLUDE_TOOLS_FLAG (CLI exclusions)
|
||||
- 4.3 - ALLOWED_TOOLS_FLAG (CLI allows)
|
||||
- 4.2 - TRUSTED_MCP_SERVER
|
||||
- 4.1 - ALLOWED_MCP_SERVER
|
||||
- 3.95 - ALWAYS_ALLOW (interactive selections)
|
||||
|
||||
### Security Constraint
|
||||
|
||||
- Extensions CANNOT contribute ALLOW rules or YOLO mode
|
||||
|
||||
## Agent System
|
||||
|
||||
### Agent Registry (`core/src/agents/registry.ts`)
|
||||
|
||||
Discovery sources:
|
||||
|
||||
1. Built-in: CodebaseInvestigator, CliHelp, Generalist, Browser
|
||||
2. User-level: `~/.gemini/agents/`
|
||||
3. Project-level: `.gemini/agents/` (requires folder trust)
|
||||
4. Extension-based: From active extensions
|
||||
|
||||
### LocalAgentExecutor (`core/src/agents/local-executor.ts`)
|
||||
|
||||
- Prompt processing: input augmentation → template expansion → system prompt
|
||||
construction
|
||||
- Uses GeminiChat for accumulating conversation
|
||||
- ChatCompressionService for history management
|
||||
- Turn loop: invoke model → extract function calls → check auth → append results
|
||||
- Termination: complete_task tool, max turns, timeout
|
||||
|
||||
### SubagentTool (`core/src/agents/subagent-tool.ts`)
|
||||
|
||||
- Extends BaseDeclarativeTool - agents invoked like standard tools
|
||||
- Read-only status checking, user hint propagation
|
||||
- Execution: validate → optional confirmation → parameter enrichment →
|
||||
SubagentToolWrapper
|
||||
|
||||
### Remote Agents
|
||||
|
||||
- A2A client manager for agent-to-agent protocol
|
||||
- Remote invocation for external agents
|
||||
- Agent acknowledgement system (security for project agents)
|
||||
|
||||
## Model System
|
||||
|
||||
### ModelConfigService
|
||||
|
||||
- **Hierarchical alias system**: children override parents
|
||||
- Resolution: alias chain → level assignment → apply overrides
|
||||
- Deep merging with array override capability
|
||||
- Fallback to `chat-base` alias for unknown models
|
||||
|
||||
### ModelRouterService
|
||||
|
||||
Sequential strategy pattern:
|
||||
|
||||
1. Fallback & Override
|
||||
2. Approval Mode Strategy
|
||||
3. Gemma Classifier (if enabled)
|
||||
4. Generic Classifier
|
||||
5. Numerical Classifier
|
||||
6. Default Strategy
|
||||
|
||||
### ModelAvailabilityService
|
||||
|
||||
Health states:
|
||||
|
||||
- **Terminal** - permanently unavailable
|
||||
- **Sticky Retry** - failed once, can retry once per turn
|
||||
- **Healthy** - no issues
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Purpose |
|
||||
| --------------------------- | --------------------------------------- |
|
||||
| ChatRecordingService | Session persistence (JSON files) |
|
||||
| ChatCompressionService | History summarization for token budgets |
|
||||
| ModelConfigService | Hierarchical model config with aliases |
|
||||
| ModelAvailabilityService | Model health tracking |
|
||||
| ModelRouterService | Model selection via strategies |
|
||||
| FolderTrustDiscoveryService | Workspace security scanning |
|
||||
| KeychainService | Credential storage |
|
||||
| LoopDetectionService | Detect repetitive agent loops |
|
||||
|
||||
## UI + Core Separation
|
||||
|
||||
### IDE Client (`core/src/ide/ide-client.ts`)
|
||||
|
||||
- Singleton managing CLI ↔ IDE communication via MCP
|
||||
- **Outbound** (CLI → IDE): `openDiff`, `closeDiff`
|
||||
- **Inbound** (IDE → CLI): `ide/contextUpdate`, `ide/diffAccepted`,
|
||||
`ide/diffRejected`
|
||||
|
||||
### Event Contract
|
||||
|
||||
```typescript
|
||||
interface IdeContextNotification {
|
||||
method: 'ide/contextUpdate';
|
||||
params: { workspaceState: { openFiles: string[]; isTrusted: boolean } };
|
||||
}
|
||||
```
|
||||
|
||||
### Confirmation Bus
|
||||
|
||||
- `TOOL_CONFIRMATION_REQUEST` / `TOOL_CONFIRMATION_RESPONSE`
|
||||
- Detail types: edit, execute, MCP, info, ask_user, exit_plan_mode
|
||||
- Async pub/sub via MessageBus
|
||||
|
||||
## Configuration (`core/src/config/config.ts` ~95KB!)
|
||||
|
||||
- Tool config: core tools, allowed/excluded, MCP servers
|
||||
- File filtering: git ignore, fuzzy search, max counts, timeouts
|
||||
- Approval modes: policy engine config
|
||||
- Experiments: feature flags (GEMINI_3_1_PRO_LAUNCHED, ENABLE_ADMIN_CONTROLS,
|
||||
etc.)
|
||||
- FolderTrust: discovery scans for commands, skills, settings, MCP, hooks
|
||||
@@ -0,0 +1,296 @@
|
||||
# Deep Dive: Key Gemini-CLI Systems
|
||||
|
||||
## Hooks System (Complete)
|
||||
|
||||
### 11 Hook Points
|
||||
|
||||
| Hook | Input | Key Output Capabilities |
|
||||
| ------------------- | -------------------------------------- | --------------------------------------------- |
|
||||
| BeforeTool | toolName, toolInput, mcpContext | Modify tool_input, block/allow, systemMessage |
|
||||
| AfterTool | toolName, toolInput, toolResponse | additionalContext, tailToolCallRequest |
|
||||
| BeforeAgent | prompt | Additional context |
|
||||
| AfterAgent | prompt, response, stopHookActive | Clear context |
|
||||
| BeforeModel | llmRequest (GenerateContentParameters) | Modify llm_request OR inject llm_response |
|
||||
| AfterModel | llmRequest, llmResponse | Modify llm_response |
|
||||
| BeforeToolSelection | llmRequest | Modify toolConfig (function list, mode) |
|
||||
| Notification | type, message, details | Suppress/modify |
|
||||
| SessionStart | source (Startup/Resume/Clear) | Additional context |
|
||||
| SessionEnd | reason (Exit/Clear/Logout/etc) | Cleanup |
|
||||
| PreCompress | trigger (Manual/Auto) | Suppress/modify |
|
||||
|
||||
### Hook Configuration Types
|
||||
|
||||
- **Runtime hooks** (HookType.Runtime): JS/TS functions, registered
|
||||
programmatically
|
||||
- **Command hooks** (HookType.Command): External shell commands with JSON I/O
|
||||
|
||||
### Exit Code Semantics (Command Hooks)
|
||||
|
||||
- 0 = Success (allowed with system message)
|
||||
- 1 = Non-blocking error (warning, continues)
|
||||
- 2+ = Blocking failure (denied, stderr as reason)
|
||||
|
||||
### Hook Decision Values
|
||||
|
||||
`'ask' | 'block' | 'deny' | 'approve' | 'allow' | undefined`
|
||||
|
||||
### Execution Strategies
|
||||
|
||||
- **Parallel** (default): Promise.all(), independent
|
||||
- **Sequential** (opt-in per hook): Chained, output→input cascading
|
||||
|
||||
### Aggregation
|
||||
|
||||
- Blocking decisions: OR logic (any block → all block)
|
||||
- Field replacement: later overrides earlier
|
||||
- Tool selection: union of allowed functions, mode precedence NONE > ANY > AUTO
|
||||
|
||||
### Trust Model
|
||||
|
||||
- Project hooks require folder trust verification
|
||||
- TrustedHooksManager at `~/.gemini/trusted-hooks.json`
|
||||
- Environment sanitized for command hooks (sensitive vars removed)
|
||||
- `GEMINI_PROJECT_DIR` injected
|
||||
|
||||
### Key Insight for Abstraction
|
||||
|
||||
Hooks fire inside gemini-cli's execution loop. When ADK controls the model:
|
||||
|
||||
- BeforeModel/AfterModel still fire because AdkGeminiModel wraps GeminiChat
|
||||
- BeforeTool/AfterTool still fire because AdkToolAdapter wraps DeclarativeTool
|
||||
- This is dewitt's solution: adapters preserve hook injection points
|
||||
|
||||
**For OpenRouter or opaque agents, hooks CANNOT fire unless the agent delegates
|
||||
model/tool calls back to gemini-cli.**
|
||||
|
||||
---
|
||||
|
||||
## Policy Engine (Complete)
|
||||
|
||||
### TOML Rule Format
|
||||
|
||||
```toml
|
||||
[[rules]]
|
||||
decision = "allow" | "deny" | "ask_user"
|
||||
priority = 0-999
|
||||
toolName = "tool_name" # wildcards: *, mcp_*, mcp_server_*
|
||||
mcpName = "server_name" # MCP server filter
|
||||
argsPattern = "regex" # matches JSON-stringified args
|
||||
commandPrefix = "cmd" # shell command prefix match
|
||||
commandRegex = "regex" # shell command regex (mutually exclusive with prefix)
|
||||
modes = ["default", "autoEdit", "yolo", "plan"]
|
||||
annotations = ["read-only", "experimental"]
|
||||
allowRedirection = true # for shell commands
|
||||
allowMessage = "..." # user-facing message on allow
|
||||
denyMessage = "..." # user-facing message on deny
|
||||
```
|
||||
|
||||
### 5-Tier Priority System
|
||||
|
||||
- Tier 5 (Admin): 5.000-5.999
|
||||
- Tier 4 (User): 4.000-4.999
|
||||
- Tier 3 (Workspace): 3.000-3.999
|
||||
- Tier 2 (Extension): 2.000-2.999
|
||||
- Tier 1 (Default): 1.000-1.999
|
||||
|
||||
Formula: `tier + (priority / 1000)`
|
||||
|
||||
### 4 Approval Modes
|
||||
|
||||
1. **default** — ASK_USER decisions prompt user
|
||||
2. **autoEdit** — File writes auto-approved with safety checking (conseca)
|
||||
3. **yolo** — All auto-approved except explicit ask_user rules
|
||||
4. **plan** — Read-only, blocks modifications, allows planning docs
|
||||
|
||||
### Shell Command Safety
|
||||
|
||||
- Parses multi-command sequences (&&, ;, ||)
|
||||
- Detects injection: $(...), `...`, <(...), >(...), --flag=$(...)
|
||||
- Each subcommand evaluated independently
|
||||
- DENY overrides everything; ASK_USER escalates; ALLOW only if all pass
|
||||
- Redirections (>) downgrade ALLOW → ASK_USER unless allowRedirection=true
|
||||
|
||||
### Security Constraints
|
||||
|
||||
- Extensions cannot contribute ALLOW rules or YOLO mode
|
||||
- Regex patterns validated for ReDoS
|
||||
- Tool name typos detected via Levenshtein distance ≤3
|
||||
- Policy file integrity: SHA-256 hash checking
|
||||
|
||||
### Key Insight for Abstraction
|
||||
|
||||
Policy is evaluated at the tool execution boundary. For the interface layer:
|
||||
|
||||
- If CLI controls tool execution → policy naturally applies
|
||||
- If agent controls tool execution internally → policy bypassed (danger!)
|
||||
- This reinforces the `pauseOnToolCalls: true` approach for ADK
|
||||
- Need a `PolicyEvaluator` interface that any executor can call
|
||||
|
||||
---
|
||||
|
||||
## Tool System (Complete)
|
||||
|
||||
### Core Abstraction Chain
|
||||
|
||||
```
|
||||
ToolBuilder (metadata + schema)
|
||||
→ build(params) validates → ToolInvocation (ready to execute)
|
||||
→ shouldConfirmExecute() → execute(signal) → ToolResult
|
||||
```
|
||||
|
||||
### DeclarativeTool Pattern
|
||||
|
||||
- `build(params)` — Validate and create invocation
|
||||
- `buildAndExecute(params)` — One-step convenience
|
||||
- `validateBuildAndExecute(params)` — Non-throwing variant
|
||||
|
||||
### BaseToolInvocation
|
||||
|
||||
- Message bus integration for policy decisions
|
||||
- Three decision paths: ALLOW → execute, DENY → reject, ASK_USER → confirm
|
||||
|
||||
### ToolResult Structure
|
||||
|
||||
- `llmContent` — For LLM conversation history
|
||||
- `returnDisplay` — For UI presentation
|
||||
- `displayContent` — Additional display formatting
|
||||
- `errorDetails` — Optional error info
|
||||
- `result` — Structured data payload
|
||||
- `tailCall` — Optional chaining requests
|
||||
|
||||
### Confirmation System (6 types)
|
||||
|
||||
1. **edit** — File modification with diff
|
||||
2. **execute** — Command execution
|
||||
3. **mcp** — MCP tool with allowlist mgmt
|
||||
4. **info** — Information-only
|
||||
5. **ask_user** — General user approval
|
||||
6. **exit_plan_mode** — Plan exit notification
|
||||
|
||||
### Confirmation Outcomes (7 values)
|
||||
|
||||
ProceedOnce, ProceedAlways, ProceedAlwaysAndSave, ProceedAlwaysServer,
|
||||
ProceedAlwaysTool, ModifyWithEditor, Cancel
|
||||
|
||||
### Tool Kinds
|
||||
|
||||
- **Mutator**: Edit, Delete, Move, Execute
|
||||
- **Read-Only**: Read, Search, Fetch
|
||||
- **Other**: Think, Agent, Communicate, Plan, SwitchMode, Other
|
||||
|
||||
### MCP Tools
|
||||
|
||||
- Naming: `mcp_<server>_<toolname>` (64-char limit)
|
||||
- Schema validation via LenientJsonSchemaValidator
|
||||
- Response types: McpTextBlock, McpMediaBlock, McpResourceBlock,
|
||||
McpResourceLinkBlock
|
||||
- Transform to GenAI Parts format
|
||||
|
||||
### Error Types (20+)
|
||||
|
||||
- **Recoverable**: INVALID_TOOL_PARAMS, FILE_NOT_FOUND,
|
||||
EDIT_NO_OCCURRENCE_FOUND, SHELL_TIMEOUT, MCP_TOOL_ERROR...
|
||||
- **Fatal**: NO_SPACE_LEFT (only one!)
|
||||
|
||||
### ModifiableTool
|
||||
|
||||
- Extends DeclarativeTool with external editor support
|
||||
- `getModifyContext()` → temp files → editor opens → `getUpdatedParams()` → diff
|
||||
|
||||
---
|
||||
|
||||
## Execution Loop (Complete)
|
||||
|
||||
### LocalAgentExecutor Flow
|
||||
|
||||
1. Collect user hints, setup deadline timer
|
||||
2. **Turn loop**: executeTurn() repeatedly until completion
|
||||
3. Per-turn: compress chat → callModel() → processFunctionCalls()
|
||||
4. On limit hit: executeFinalWarningTurn() with 60s grace period
|
||||
5. Return OutputObject { result, terminate_reason }
|
||||
|
||||
### AgentTerminateMode
|
||||
|
||||
GOAL | TIMEOUT | MAX_TURNS | ABORTED | ERROR | ERROR_NO_COMPLETE_TASK_CALL
|
||||
|
||||
### SubagentTool Architecture
|
||||
|
||||
```
|
||||
Parent Agent
|
||||
└─ SubagentTool (wraps AgentDefinition as DeclarativeTool)
|
||||
└─ SubagentToolWrapper (routes by agent kind)
|
||||
├─ LocalSubagentInvocation → LocalAgentExecutor
|
||||
├─ RemoteAgentInvocation → A2AClientManager
|
||||
└─ BrowserAgentInvocation
|
||||
```
|
||||
|
||||
### Agent Types
|
||||
|
||||
- `LocalAgentDefinition` — kind: 'local', has promptConfig, modelConfig,
|
||||
runConfig, toolConfig
|
||||
- `RemoteAgentDefinition` — kind: 'remote', has agentCardUrl, auth config
|
||||
|
||||
### Key Defaults
|
||||
|
||||
- DEFAULT_MAX_TURNS = 15
|
||||
- DEFAULT_MAX_TIME_MINUTES = 5
|
||||
- A2A_TIMEOUT = 1800000 (30 min for remote agents)
|
||||
|
||||
---
|
||||
|
||||
## Services/Config (Complete)
|
||||
|
||||
### ModelConfigService
|
||||
|
||||
- **Alias chains**: Inheritance with `extends`, merged root-to-leaf
|
||||
- **Overrides**: Contextual (model, scope, retry, isChatModel), sorted by
|
||||
specificity
|
||||
- **Runtime registration**: Dynamic aliases and overrides
|
||||
- **Deep merge**: Objects merged, arrays replaced entirely
|
||||
|
||||
### ModelRouterService (Strategy Chain)
|
||||
|
||||
1. Fallback & Override → 2. Approval Mode → 3. Gemma Classifier → 4. Generic
|
||||
Classifier → 5. Numerical Classifier → 6. Default
|
||||
|
||||
### ModelAvailabilityService
|
||||
|
||||
- Terminal (permanent), Sticky_retry (one retry per turn), Healthy
|
||||
- `selectFirstAvailable()` iterates fallback chain
|
||||
- `resetTurn()` at turn boundaries enables fresh retries
|
||||
|
||||
### Config (~95KB!)
|
||||
|
||||
Central dependency injection. Initializes: ModelAvailabilityService →
|
||||
ModelConfigService → FolderTrustDiscoveryService → PolicyEngine →
|
||||
FileDiscoveryService → GitService → ToolRegistry → MCP → GeminiClient →
|
||||
HookSystem
|
||||
|
||||
### CoreEventEmitter (UI Events)
|
||||
|
||||
Event types: UserFeedback, ModelChanged, ConsoleLog, Output, RetryAttempt,
|
||||
ConsentRequest, McpProgress, Hook, QuotaChanged
|
||||
|
||||
Backlog buffering (max 10,000) with head-pointer eviction and auto-compaction.
|
||||
|
||||
### Scheduler Types
|
||||
|
||||
```typescript
|
||||
ToolCallRequestInfo {
|
||||
callId, name, args, originalRequestName,
|
||||
isClientInitiated, prompt_id, checkpoint, traceId,
|
||||
parentCallId, schedulerId
|
||||
}
|
||||
ToolCallResponseInfo {
|
||||
callId, responseParts, resultDisplay, error, errorType,
|
||||
outputFile, contentLength, data
|
||||
}
|
||||
CoreToolCallStatus: Validating → AwaitingApproval → Scheduled → Executing → Success|Error|Cancelled
|
||||
```
|
||||
|
||||
### FolderTrust
|
||||
|
||||
Scans: commands (.toml), skills (SKILL.md), settings.json, MCP servers, hooks
|
||||
Security warnings: auto-approved tools, autonomous agents, disabled trust,
|
||||
disabled sandbox Pattern: discovery → review → execution (no code runs during
|
||||
scan)
|
||||
@@ -0,0 +1,349 @@
|
||||
# Interface Priority Analysis & Open Questions
|
||||
|
||||
## The Big Picture
|
||||
|
||||
We're defining **framework-agnostic interfaces** that allow gemini-cli to:
|
||||
|
||||
1. Keep its existing execution loop working unchanged (Legacy path)
|
||||
2. Swap in ADK as an alternative runtime via config flag
|
||||
3. Eventually support OpenRouter or other agent backends
|
||||
4. Maintain all existing CLI behavior: hooks, policies, confirmations, UI events
|
||||
|
||||
## Proposed Interface Layers (Priority Order)
|
||||
|
||||
---
|
||||
|
||||
### P0 (Critical Path - Must Define First)
|
||||
|
||||
#### 1. AgentEvent / Event Stream Contract
|
||||
|
||||
**Why first:** Everything else consumes or produces these events. The UI renders
|
||||
them. The hooks intercept them. The adapters translate to/from them.
|
||||
|
||||
**Key decision:** Merge Dewitt's simpler model with Coworker's richer model?
|
||||
|
||||
**Recommendation:** Coworker's approach is more complete. Key additions:
|
||||
|
||||
- `threadId` for sub-agent tracking (AG-UI has `parentRunId`)
|
||||
- `tool_update` for progress on long-running tools
|
||||
- `elicitation_request/response` as first-class (not just tool_confirmation)
|
||||
- `usage` event for token tracking
|
||||
- `_meta` escape hatch (matches AG-UI's extensibility philosophy)
|
||||
- `initialize` event (matches AG-UI's RunStarted)
|
||||
|
||||
**Open questions:**
|
||||
|
||||
- Do we need AG-UI's start/content/end triple pattern for streaming? Or is
|
||||
yielding partial events sufficient?
|
||||
- How do ContentPart types map to existing gemini-cli Part types?
|
||||
- Should events carry a `source` field? (useful for hook attribution)
|
||||
|
||||
#### 2. Agent Interface
|
||||
|
||||
**Why second:** This is the primary abstraction that LocalAgentExecutor, ADK
|
||||
adapters, and future OpenRouter adapters all implement.
|
||||
|
||||
**Key decision:** Dewitt's `runAsync/runEphemeral` vs Coworker's
|
||||
`send(Trajectory|string)`
|
||||
|
||||
**Recommendation:** Hybrid approach:
|
||||
|
||||
- Dewitt's `runAsync/runEphemeral` split is ADK-aligned and cleaner for the
|
||||
factory pattern
|
||||
- BUT add Coworker's elicitation support via AgentSend union type
|
||||
- The Trajectory concept is powerful but may be too opinionated for Phase 2
|
||||
|
||||
```
|
||||
Agent<TInput, TOutput>
|
||||
name: string
|
||||
description: string
|
||||
runAsync(input, options) → AsyncGenerator<AgentEvent, TOutput>
|
||||
runEphemeral(input, options) → AsyncGenerator<AgentEvent, TOutput>
|
||||
```
|
||||
|
||||
**Open questions:**
|
||||
|
||||
- Should Agent also support `send()` for mid-stream interactions (elicitations)?
|
||||
- How does AbortSignal propagate through the adapter boundary?
|
||||
- Do we need a `capabilities` field (supports elicitation? supports HITL? etc.)?
|
||||
|
||||
#### 3. Tool Execution Contract
|
||||
|
||||
**Why third:** Tools are the primary action mechanism. Both the policy engine
|
||||
and hooks system wrap tool execution.
|
||||
|
||||
**What needs abstracting:**
|
||||
|
||||
- Tool declaration (name, schema) — already somewhat generic via JSON Schema
|
||||
- Tool execution (args → result)
|
||||
- Tool confirmation flow (ASK_USER → user decision → proceed/deny)
|
||||
- Tool result shape (llmContent + displayContent + error + tailCalls)
|
||||
|
||||
**Key decision:** Keep DeclarativeTool pattern or flatten to a simpler
|
||||
interface?
|
||||
|
||||
**Recommendation:** Define a minimal `ToolExecutor` interface:
|
||||
|
||||
```
|
||||
ToolExecutor {
|
||||
name: string
|
||||
description: string
|
||||
schema: JSONSchema
|
||||
execute(args, context): Promise<ToolResult>
|
||||
requiresConfirmation?(args, context): Promise<boolean>
|
||||
}
|
||||
```
|
||||
|
||||
DeclarativeTool remains the concrete implementation. ADK's BaseTool adapts to
|
||||
this.
|
||||
|
||||
**Open questions:**
|
||||
|
||||
- How do MCP tools fit? They already have their own protocol.
|
||||
- Tool annotations (destructive hints) — should these be in the interface?
|
||||
- Long-running tools need progress reporting — how does this interact with
|
||||
tool_update events?
|
||||
|
||||
---
|
||||
|
||||
### P1 (Important - Define After P0)
|
||||
|
||||
#### 4. Policy / Permission Interface
|
||||
|
||||
**Why important:** Every tool call goes through policy. External agents need
|
||||
policy enforcement too.
|
||||
|
||||
**Current state:** gemini-cli has a sophisticated TOML-based policy engine with
|
||||
tiered priorities. ADK-TS has a simpler SecurityPlugin with PolicyOutcome
|
||||
(DENY/CONFIRM/ALLOW).
|
||||
|
||||
**What needs abstracting:**
|
||||
|
||||
```
|
||||
PolicyEngine {
|
||||
evaluate(toolName, args, context): PolicyDecision // ALLOW | DENY | ASK_USER
|
||||
getExcludedTools(): string[] // Tools statically denied
|
||||
}
|
||||
```
|
||||
|
||||
**Key decision:** Do external agents (OpenRouter, etc.) get the same policy
|
||||
enforcement?
|
||||
|
||||
**Open questions:**
|
||||
|
||||
- If an ADK agent calls a tool internally, does gemini-cli's policy apply?
|
||||
- With `pauseOnToolCalls: true` in ADK, the CLI controls execution — but what
|
||||
about headless mode?
|
||||
- How do agent-level policies work? (allow/deny entire agents, not just tools)
|
||||
- Should policy be a middleware (AG-UI pattern) or a callback (ADK plugin
|
||||
pattern)?
|
||||
|
||||
#### 5. Hooks Interface
|
||||
|
||||
**Why important:** Hooks are a major gemini-cli feature. They need to work
|
||||
regardless of which agent backend runs.
|
||||
|
||||
**Current state:** 11 hook types firing at specific lifecycle points.
|
||||
|
||||
**What needs abstracting:**
|
||||
|
||||
- Hook lifecycle must be backend-agnostic
|
||||
- BeforeModel/AfterModel hooks need to work even when ADK controls the model
|
||||
- BeforeTool/AfterTool hooks need to intercept regardless of who executes the
|
||||
tool
|
||||
|
||||
**Key challenge:** When ADK runs the model internally, gemini-cli hooks can't
|
||||
easily intercept. **Dewitt's solution:** ADK uses gemini-cli's model via
|
||||
AdkGeminiModel adapter — hooks fire inside GeminiChat.
|
||||
|
||||
**Open questions:**
|
||||
|
||||
- If OpenRouter runs the model, how do BeforeModel/AfterModel hooks work?
|
||||
- Do we need a "model steering" abstraction (injecting context mid-stream)?
|
||||
- Can hooks be expressed as AG-UI middleware? (intercept event stream)
|
||||
|
||||
#### 6. Model / LLM Interface
|
||||
|
||||
**Why important:** Model abstraction enables swapping LLM providers.
|
||||
|
||||
**Dewitt's approach:** Exposes Model interface, ADK uses it via AdkGeminiModel
|
||||
adapter. **Coworker's approach:** Model is internal to Agent (no separate Model
|
||||
interface).
|
||||
|
||||
**Recommendation:** Keep Dewitt's separate Model interface BUT make it
|
||||
provider-agnostic:
|
||||
|
||||
- Remove `@google/genai` types from the interface signature
|
||||
- Define generic Message/Content types
|
||||
- Model interface is an implementation detail, not part of the Agent contract
|
||||
|
||||
**Open questions:**
|
||||
|
||||
- Can we define a truly provider-agnostic Model interface?
|
||||
- Or is the Model always tied to the agent backend? (ADK uses Gemini, OpenRouter
|
||||
uses whatever)
|
||||
- Model routing (choosing which model) — is this a concern of the Model
|
||||
interface or a separate service?
|
||||
|
||||
---
|
||||
|
||||
### P2 (Important but Can Follow)
|
||||
|
||||
#### 7. Session / State Interface
|
||||
|
||||
**Current state:** gemini-cli uses ChatRecordingService (JSON files). ADK uses
|
||||
Session with BaseSessionService.
|
||||
|
||||
**What needs abstracting:**
|
||||
|
||||
- Session creation/retrieval
|
||||
- State persistence across turns
|
||||
- History/trajectory management
|
||||
|
||||
**Open questions:**
|
||||
|
||||
- Does the trajectory (coworker's concept) replace gemini-cli's chat recording?
|
||||
- Should session state be shared between gemini-cli and the agent backend?
|
||||
|
||||
#### 8. Elicitation / User Interaction Interface
|
||||
|
||||
**What it covers:** Model fallback dialogs, tool confirmations, Ctrl+B
|
||||
interrupts, user questions
|
||||
|
||||
**Current state:** gemini-cli uses ConfirmationBus + MessageBus. AG-UI uses
|
||||
frontend tools.
|
||||
|
||||
**Open questions:**
|
||||
|
||||
- Is elicitation just a special case of tool calls (AG-UI approach)?
|
||||
- Or is it a first-class event type (coworker's approach)?
|
||||
- How does Ctrl+B (cancel/interrupt) propagate through the agent boundary?
|
||||
|
||||
#### 9. Configuration / Capability Discovery
|
||||
|
||||
**What it covers:** Feature flags, experiment settings, agent capabilities
|
||||
|
||||
**Open questions:**
|
||||
|
||||
- How does an external agent declare its capabilities?
|
||||
- Does OpenRouter support HITL? Elicitation? Tool confirmation? Each agent may
|
||||
differ.
|
||||
- Need a `capabilities` negotiation at connection time?
|
||||
|
||||
---
|
||||
|
||||
### P3 (Future / Can Defer)
|
||||
|
||||
#### 10. A2UI / Rich UI Interface
|
||||
|
||||
- Declarative UI generation from agents
|
||||
- Not critical for Phase 2 but important for differentiation
|
||||
|
||||
#### 11. Memory / Artifact Interface
|
||||
|
||||
- ADK has memory/artifact services
|
||||
- gemini-cli has ChatRecordingService + memory tools
|
||||
- Can standardize later
|
||||
|
||||
#### 12. Telemetry / Observability Interface
|
||||
|
||||
- Both systems have telemetry
|
||||
- Can standardize later
|
||||
|
||||
---
|
||||
|
||||
## Critical Open Questions (Need Team Discussion)
|
||||
|
||||
### 1. OpenRouter Integration Model
|
||||
|
||||
**Question:** When OpenRouter (or any external agent) is used, what does the
|
||||
integration look like?
|
||||
|
||||
**Option A: Full Agent Interface** — OpenRouter implements the Agent interface
|
||||
directly
|
||||
|
||||
- Pro: Clean, uniform
|
||||
- Con: OpenRouter doesn't support HITL, hooks, policies natively
|
||||
|
||||
**Option B: ACP Shim** — Agent Communication Protocol between CLI and external
|
||||
agents
|
||||
|
||||
- Pro: Standards-based
|
||||
- Con: Additional protocol layer, may be premature
|
||||
|
||||
**Option C: Model-only Integration** — OpenRouter is just an alternative Model,
|
||||
not Agent
|
||||
|
||||
- Pro: Simpler, leverages existing agent loop
|
||||
- Con: Doesn't support OpenRouter-specific features
|
||||
|
||||
**Recommendation:** Start with Option C (model-only). OpenRouter provides an LLM
|
||||
endpoint. Gemini-cli's own agent loop handles tools, policies, hooks. This means
|
||||
defining a provider-agnostic Model interface is the key enabler.
|
||||
|
||||
### 2. Tool Execution: Client-side vs Agent-side
|
||||
|
||||
**Question:** Who executes tools — the CLI or the agent backend?
|
||||
|
||||
**Option A: Always client-side** (CLI executes, agent suspends)
|
||||
|
||||
- ADK: `pauseOnToolCalls: true`
|
||||
- Pro: CLI maintains control, policies enforced, hooks fire
|
||||
- Con: Higher latency, more round-trips
|
||||
|
||||
**Option B: Agent-side execution** (agent runs tools internally)
|
||||
|
||||
- Pro: Faster, simpler
|
||||
- Con: Bypasses CLI policies, hooks, confirmations
|
||||
|
||||
**Option C: Configurable** — CLI decides per-tool or per-agent
|
||||
|
||||
- Pro: Flexible
|
||||
- Con: Complex
|
||||
|
||||
**Recommendation:** Option A for safety-critical CLI use case. Option B only for
|
||||
trusted/sandboxed sub-agents.
|
||||
|
||||
### 3. Model Steering (Hooks that inject context mid-stream)
|
||||
|
||||
**Question:** How do user-local hooks (like injecting project context) work with
|
||||
external agents?
|
||||
|
||||
**Answer:** They can only work if:
|
||||
|
||||
- The CLI controls the model (via Model interface adapter) — then BeforeModel
|
||||
hook injects context
|
||||
- OR the agent supports a "system instruction update" mechanism
|
||||
|
||||
For OpenRouter: model steering works because CLI controls the model call. For
|
||||
ADK: model steering works because AdkGeminiModel wraps GeminiChat. For fully
|
||||
opaque agents: model steering **cannot work** — this is a known limitation.
|
||||
|
||||
### 4. Elicitation Flow
|
||||
|
||||
**Question:** When the agent needs user input (model fallback, clarification),
|
||||
how does it work?
|
||||
|
||||
**For CLI-controlled agents:** Agent yields an elicitation_request event → CLI
|
||||
renders prompt → user responds → CLI sends response back via session.stream({
|
||||
kind: 'elicitation_response', ... }) to resume
|
||||
|
||||
**For external agents:** Agent uses A2A protocol or similar to send elicitation
|
||||
→ CLI bridges the request to user → response sent back via protocol
|
||||
|
||||
**Key insight:** Elicitation is fundamentally about the agent SUSPENDING and
|
||||
waiting for user input. ADK already supports this via `pauseOnToolCalls`. Can we
|
||||
generalize to `pauseOnElicitation`?
|
||||
|
||||
### 5. Sub-agent Identity and Policies
|
||||
|
||||
**Question:** When a sub-agent spawns, does it inherit parent policies? Get its
|
||||
own?
|
||||
|
||||
**Current gemini-cli behavior:** Sub-agents registered as tools, go through same
|
||||
policy engine. **ADK behavior:** Sub-agents are child nodes in agent tree, get
|
||||
parent's plugins.
|
||||
|
||||
**Recommendation:** Sub-agents inherit parent policy context. Additional
|
||||
restrictions can be layered (e.g., sub-agent X cannot use shell tool). This is
|
||||
already how gemini-cli works.
|
||||
@@ -0,0 +1,438 @@
|
||||
# Architectural Design: Gemini CLI to ADK Migration
|
||||
|
||||
| Authors: [Adam Weidman](mailto:adamfweidman@google.com) Contributors: Reviewers: *See section [Status of this document](#status-of-this-document).* | Status: Draft Last revised: Apr 7, 2026 Visibility: Confidential |
|
||||
| :--- | :--- |
|
||||
|
||||
---
|
||||
|
||||
# Goal
|
||||
|
||||
To migrate the Gemini CLI backend execution engine from its legacy fragmented loop structure to the Agent Development Kit (ADK). This migration will unify how agents and subagents are orchestrated, simplify state persistence, and expose a standard `AgentSession` interface for the CLI, future SDK surfaces, and subagent execution.
|
||||
|
||||
---
|
||||
|
||||
# Context
|
||||
|
||||
Over time, Gemini CLI has accumulated complex runtime behaviors: multi-tier tool scheduling, policy-driven approvals, payload masking, dynamic routing, and fine-grained telemetry. Integrating these with ADK requires a clean boundary that preserves Gemini CLI semantics without forking ADK core behavior.
|
||||
|
||||
The key migration boundary is:
|
||||
|
||||
- ADK runtime semantics -> Gemini CLI `AgentProtocol` / `AgentSession`
|
||||
- ADK `Event` stream -> Gemini CLI `AgentEvent` stream
|
||||
|
||||
That boundary, not the model wrapper alone, is the architectural center of this design.
|
||||
|
||||
---
|
||||
|
||||
# Current State and Proposed Mappings
|
||||
|
||||
The following analysis maps existing Gemini CLI components onto ADK capabilities, citing both repositories (`gemini-cli` and `adk-js`).
|
||||
|
||||
## Core Runtime Architecture
|
||||
|
||||
The migration uses one shared ADK-backed runtime core. Every orchestrated agent, including subagents, is exposed through the same external session contract:
|
||||
|
||||
- **Top-level CLI agent** -> `AgentSession`
|
||||
- **Future SDK entry point** -> `AgentSession`
|
||||
- **Subagent execution** -> `AgentSession`
|
||||
|
||||
The runtime owns:
|
||||
|
||||
- ADK runner/session lifecycle
|
||||
- tool execution
|
||||
- policy integration
|
||||
- routing, availability, compaction, and masking hooks
|
||||
- persistence integration
|
||||
|
||||
The adapters own:
|
||||
|
||||
- `streamId` timing guarantees
|
||||
- replay / reattach behavior
|
||||
- translation from ADK `Event` to Gemini CLI `AgentEvent`
|
||||
- top-level versus subagent event projection
|
||||
- projection of a child `AgentSession` into parent-facing tool or thread events when a subagent is embedded inside another agent
|
||||
|
||||
The minimum shape is:
|
||||
|
||||
```typescript
|
||||
interface PipelineServices {
|
||||
run(
|
||||
request: LlmRequest,
|
||||
baseModel: BaseLlm,
|
||||
stream: boolean,
|
||||
): AsyncGenerator<LlmResponse, void>;
|
||||
connect(
|
||||
request: LlmRequest,
|
||||
baseModel: BaseLlm,
|
||||
): Promise<BaseLlmConnection>;
|
||||
}
|
||||
|
||||
class GcliAgentModel extends BaseLlm {
|
||||
constructor(
|
||||
private baseModel: BaseLlm,
|
||||
private pipeline: PipelineServices,
|
||||
) {
|
||||
super({model: 'gcli-consolidated'});
|
||||
}
|
||||
|
||||
async *generateContentAsync(
|
||||
request: LlmRequest,
|
||||
stream = false,
|
||||
): AsyncGenerator<LlmResponse, void> {
|
||||
yield* this.pipeline.run(request, this.baseModel, stream);
|
||||
}
|
||||
|
||||
async connect(request: LlmRequest): Promise<BaseLlmConnection> {
|
||||
return this.pipeline.connect(request, this.baseModel);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Design rule:
|
||||
|
||||
- request mutation stays in the pipeline
|
||||
- all orchestrated agents expose the same `AgentSession` contract
|
||||
- session lifecycle, replay, approvals, and subagent projection stay in the runtime/adapters
|
||||
|
||||
`AdkAgentService` is the composition root for this architecture. It creates and resumes both top-level and subagent `AgentSession`s, builds scoped registries and message-bus instances, wires policy and approval bridges, and embeds child sessions through projection adapters rather than through a separate subagent runtime. See Appendix A for the intended initialization shape.
|
||||
|
||||
Composition rule:
|
||||
|
||||
- stateful tools are cloned or reinstantiated per session when needed; stateless tools may be shared
|
||||
- MCP discovery is shared at the manager layer but registered into session-local tool, prompt, and resource registries
|
||||
- `AgentLoopContext` is decomposed into pipeline config, tool/subagent config, session services, callback bridges, and UI projection rather than passed through as a single runtime object
|
||||
- file persistence remains Gemini CLI-owned through `GcliFileSessionService extends BaseSessionService`
|
||||
|
||||
Persistence rule:
|
||||
|
||||
- persisted history is an append-only event log plus derived app/user/session state
|
||||
- the session service provides atomic append, crash-safe recovery, and single-writer enforcement
|
||||
- rewind truncates the event log, recomputes derived state, and invalidates confirmation/resumption state past the rewind point
|
||||
|
||||
## 3.1 Authentication Flexibility
|
||||
|
||||
The CLI resolves distinct authentication flows (OAuth, ADC, Compute metadata) using standard Google libraries.
|
||||
|
||||
* **Current State:** Resolved in `packages/core/src/code_assist/oauth2.ts` based on `AuthType`.
|
||||
* OAuth (`LOGIN_WITH_GOOGLE`)
|
||||
* Compute Metadata Server (`COMPUTE_ADC`)
|
||||
* **Constraint:** Standard `Gemini` construction in `adk-js/core/src/models/google_llm.ts` still selects backend from constructor-level `apiKey` or Vertex config. It does **not** natively accept Gemini CLI's `AuthClient`-driven auth shape.
|
||||
* **Proposed ADK Mapping:** Phase 1 keeps auth in `GcliAgentModel`. The pipeline resolves refreshed credentials and injects them through `request.config.httpOptions.headers` for unary requests and `request.liveConnectConfig.httpOptions.headers` for live connections.
|
||||
* **Design position:** This is a **bridge**, not native ADK auth parity. Long-term cleanup is either:
|
||||
* a `CodeAssistLlm extends BaseLlm`, or
|
||||
* an upstream ADK auth-provider abstraction.
|
||||
|
||||
```typescript
|
||||
request.config ??= {};
|
||||
request.config.httpOptions ??= {};
|
||||
request.config.httpOptions.headers = {
|
||||
...request.config.httpOptions.headers,
|
||||
Authorization: `Bearer ${await auth.getAccessToken()}`,
|
||||
};
|
||||
```
|
||||
|
||||
## 3.2 Model Steering and Mid-Stream Injection
|
||||
|
||||
User interjections (hints) course-correct the loop mid-turn.
|
||||
|
||||
* **Current State:** Steering today is tied to the legacy loop and injection services.
|
||||
* **Proposed ADK Mapping (Next-Step Steering):** Supported in Phase 1. `beforeModelCallback` can read queued hints and mutate the next outbound request.
|
||||
* **Proposed ADK Mapping (True Real-Time Interrupt):** Still blocked. TypeScript ADK live runtime is not complete yet, and input-stream semantics are not a stable dependency.
|
||||
* **Phase 1 behavior:** user interjections are queued and prepended to the next model request at tool or turn boundaries. True in-place turn interruption remains out of scope.
|
||||
|
||||
## 3.3 State Management and Token Compaction
|
||||
|
||||
The CLI truncates large tool responses and summarizes older history to protect token budgets.
|
||||
|
||||
* **Current State:** `ChatCompressionService` in `packages/core/src/context/chatCompressionService.ts` implements reverse token budgeting and a two-phase verification loop.
|
||||
* **Proposed ADK Mapping:** Compaction remains a Gemini CLI-owned history processor invoked by the request pipeline.
|
||||
* **Design position:** Phase 1 does **not** force this onto ADK `BaseContextCompactor`. Gemini CLI compression is currently an outbound-history projection with truncation plus summary/verification sub-calls, while ADK's native compactor path is event-log-centric and mutates session history.
|
||||
* **Design choice:** In Phase 1, compaction mutates **outgoing request history only**. The persisted session event log remains canonical.
|
||||
* **Utility calls:** Compaction sub-calls use the same routing, auth, and availability pipeline as primary model calls.
|
||||
|
||||
## 3.4 Model Configuration and Hierarchical Overrides
|
||||
|
||||
Dynamic aliasing (for example, temperature scoped to specific sub-commands).
|
||||
|
||||
* **Current State:** Managed by `ModelConfigService`.
|
||||
* **Proposed ADK Mapping:** Resolution stays **request-scoped**, not session-init-scoped.
|
||||
* **Design choice:** The session stores requested model and override state. The runtime resolves the concrete model and temperature on each request, including retries, subagents, and utility calls.
|
||||
|
||||
## 3.5 Universal Policy Enforcement (TOML Rules)
|
||||
|
||||
Tiered workspace restrictions (for example, read-only tools in untrusted folders).
|
||||
|
||||
* **Current State:** Intercepted at tool scheduling time in legacy scheduler loops.
|
||||
* **Proposed ADK Mapping (Container):** Standardize on ADK `SecurityPlugin`.
|
||||
* **Proposed ADK Mapping (Decision Brain):** Implement `GcliPolicyEngineAdapter implements BasePolicyEngine`.
|
||||
* **Richer context:** The adapter is fed session/mode/subagent/MCP metadata from the runtime so current Gemini CLI policy semantics are preserved as closely as possible.
|
||||
* **Phase 1 suspension flow:** current tool approvals use existing Gemini CLI callbacks. This is intentionally **non-native** and **non-resumable across process death**.
|
||||
* **Long-term target:** move the same policy decisions onto native ADK confirmation/resumption semantics once the broader live/elicitation surface is ready.
|
||||
|
||||
```typescript
|
||||
interface GcliPolicyBridge {
|
||||
evaluate(context: ToolCallPolicyContext): Promise<PolicyCheckResult>;
|
||||
}
|
||||
|
||||
export class GcliPolicyEngineAdapter implements BasePolicyEngine {
|
||||
constructor(private readonly policyBridge: GcliPolicyBridge) {}
|
||||
|
||||
async evaluate(context: ToolCallPolicyContext): Promise<PolicyCheckResult> {
|
||||
return this.policyBridge.evaluate(context);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3.6 Telemetry and Observability (Clearcut Tracking)
|
||||
|
||||
Hardware metrics, token counts, and step durations.
|
||||
|
||||
* **Current State:** `ClearcutLogger` reads system metrics and relies on deep scheduler hooks for latency accounting.
|
||||
* **Proposed ADK Mapping:** Use a combination of:
|
||||
* passive event-stream observation, and
|
||||
* explicit runtime instrumentation where passive ADK events are insufficient.
|
||||
* **Correlation rule:** tool timing is keyed by `functionCall.id`, **not** by `event.id`.
|
||||
* **Design position:** Passive stream interception alone is not assumed to provide full parity.
|
||||
|
||||
## 3.7 Dynamic Model Routing and Configurability
|
||||
|
||||
Banning a model mid-turn, auto-routing via classifiers, and falling back dynamically without reinitializing the session.
|
||||
|
||||
* **Current State:** Managed by `ModelRouterService` and a chain of `RoutingStrategy` implementations which require the full `RoutingContext` (`history`, `request`, `AbortSignal`).
|
||||
* **Proposed ADK Mapping:** Mostly implementable now, but not “100% possible today” without bridge logic.
|
||||
* **Design choice:** The runtime constructs a proper `RoutingContext` from:
|
||||
* canonical session history
|
||||
* the pending user request
|
||||
* requested model
|
||||
* abort signal
|
||||
* **Execution point:** routing remains request-scoped and runs before final dispatch.
|
||||
* **Model banning:** treated as routing/fallback selection, not as a synthetic terminal model error.
|
||||
|
||||
## 3.8 Fallbacks and Availability Management
|
||||
|
||||
Ensuring availability by retrying or switching models when rate limits (429s) or terminal faults occur.
|
||||
|
||||
* **Current State:** Managed by `ModelAvailabilityService` and `ModelPool`.
|
||||
* **Proposed ADK Mapping (Preflight):** availability and fallback selection run before dispatch and before routing commits to a concrete model.
|
||||
* **Proposed ADK Mapping (Post-failure):** handled separately by the runtime. Availability state mutation and retry decisions are not treated as pure request preprocessing.
|
||||
* **Global Application:** utility calls use the same availability/fallback services as primary model calls.
|
||||
* **Retry safety rule:** automatic full-turn replay is allowed only before any side-effecting tool has executed. After side effects, the runtime surfaces the failure and requires explicit user action.
|
||||
* **Phase 1 transition path:** approvals and fallback prompts continue to use existing Gemini CLI callbacks. The doc treats this as an internal bridge, not as an existing standard ADK elicitation API.
|
||||
|
||||
## 3.9 State-Driven Mode Switching (Plan Mode)
|
||||
|
||||
Dynamically shifting system prompts and active tools when users switch interaction tiers (for example, Chat Mode to Plan Mode).
|
||||
|
||||
* **Current State:** Toggled via `/plan`, which changes `ApprovalMode` and related legacy scheduler behavior.
|
||||
* **Proposed ADK Mapping (Dynamic Prompt):** Use `InstructionProvider` in `LlmAgentConfig.instruction`.
|
||||
* **Proposed ADK Mapping (Dynamic Tooling):** Use a custom `BaseToolset` whose filter is derived from session mode.
|
||||
* **Single source of truth:** mode is session/runtime state derived from current approval mode; prompt, toolset, policy, routing, and UI all consume the same state.
|
||||
|
||||
```typescript
|
||||
export class GcliModeAwareToolset extends BaseToolset {
|
||||
constructor(
|
||||
private readonly chatTools: BaseTool[],
|
||||
private readonly planTools: BaseTool[],
|
||||
) {
|
||||
super(() => true);
|
||||
}
|
||||
|
||||
async getTools(context?: ReadonlyContext): Promise<BaseTool[]> {
|
||||
const isPlan = context?.state.get('plan_mode') === true;
|
||||
return isPlan ? this.planTools : this.chatTools;
|
||||
}
|
||||
|
||||
async close(): Promise<void> {}
|
||||
}
|
||||
```
|
||||
|
||||
## 3.10 Tool Output Masking
|
||||
|
||||
Managing context window efficiency by offloading bulky tool outputs (for example, shell logs and large file reads) to files.
|
||||
|
||||
* **Current State:** `ToolOutputMaskingService` in `packages/core/src/context/toolOutputMaskingService.ts`.
|
||||
* **Proposed ADK Mapping:** masking runs on **outgoing request history only**, after compaction.
|
||||
* **Design choice:** persisted session history remains the canonical event log. Masking artifacts are session-scoped files referenced from the outbound request projection.
|
||||
|
||||
---
|
||||
|
||||
# 5. Known Gaps in ADK (Gating Blockers)
|
||||
|
||||
This section highlights existing gaps in standard ADK that prevent a seamless cutover without bridge logic or upstream changes.
|
||||
|
||||
## 5.1 Real-Time User Message Injections (Aborted Turns)
|
||||
|
||||
While next-step steering is possible today using `beforeModelCallback`, true real-time interruption requires:
|
||||
|
||||
- stable input-stream support, and
|
||||
- a complete TypeScript live runtime
|
||||
|
||||
Until then, the supported behavior is queued next-step steering at model boundaries, not mid-stream interruption.
|
||||
|
||||
## 5.2 Conversation Rewind and State Reversal
|
||||
|
||||
Translating manual trajectory drops to ADK runtime state is cumbersome. While Python ADK supports rollback, TypeScript ADK does not yet support it natively.
|
||||
|
||||
* **Resolution Strategy:** Gemini CLI implements rewind in `GcliFileSessionService`, but **not** as a shallow JSON edit. Rewind truncates the event log, recomputes derived state, and invalidates any confirmation/resumption state past the rewind point.
|
||||
|
||||
## 5.3 Phase 1 Non-Goals
|
||||
|
||||
To keep the migration surface bounded, Phase 1 intentionally excludes:
|
||||
|
||||
- non-interactive surfacing of `elicitation_request` / `elicitation_response`
|
||||
- true live/bidi interruption semantics
|
||||
- native ADK confirmation/resumption parity for approvals and fallback prompts
|
||||
|
||||
---
|
||||
|
||||
# Long-Term Vision: Unification of Agents and Subagents
|
||||
|
||||
The long-term vision is that subagents and the primary agent share:
|
||||
|
||||
- the same runtime core
|
||||
- the same tool definitions
|
||||
- the same policy constraints
|
||||
- the same configuration schemas
|
||||
- the same orchestration contract: `AgentSession`
|
||||
|
||||
What may differ is the **embedding adapter**:
|
||||
|
||||
- standalone/top-level agent -> consumed directly as `AgentSession`
|
||||
- subagent embedded by a parent agent -> projected from its `AgentSession` into parent-facing tool or child-thread events
|
||||
|
||||
For SDK-first unification, Gemini CLI orchestration targets `AgentSession`, not a specific ADK tool abstraction. When a subagent must be exposed to a parent model as a tool, Gemini CLI wraps that child `AgentSession` with a `FunctionTool` or custom `BaseTool` projection. `AgentTool` remains the native ADK nested-agent option if we later want full ADK-native nested-agent semantics.
|
||||
|
||||
---
|
||||
|
||||
# Migration Sequence and Unification Checklist
|
||||
|
||||
The migration remains non-sequential, but each phase has an explicit success condition:
|
||||
|
||||
- [ ] **Non-interactive session parity** `#22699`
|
||||
output/error parity, basic replay correctness, feature-flagged rollout
|
||||
- [ ] **Interactive session parity** `#22701`
|
||||
projected event parity, approval bridge wiring, no live-interrupt claim
|
||||
- [ ] **Subagent adapter parity** `#22700`
|
||||
tool isolation, activity projection, policy/routing parity for child runs
|
||||
- [ ] **ADK session conformance** `#22974`
|
||||
`AgentSession` timing/replay guarantees preserved by the adapter
|
||||
- [ ] **Skills parity** `#22966`
|
||||
skills work through the shared runtime without special legacy paths
|
||||
- [ ] **Policy and confirmation parity** `#22964`
|
||||
phase-1 callback bridge stable; native confirmation migration scoped separately
|
||||
- [ ] **Compaction quality parity** `#22979`
|
||||
comparable summarization and truncation quality to current behavior
|
||||
|
||||
---
|
||||
|
||||
# Appendix A: Initialization Sketch
|
||||
|
||||
The main agent and subagents share the same composition root. The difference is whether the resulting `AgentSession` is consumed directly or projected into a parent session.
|
||||
|
||||
```typescript
|
||||
shared = {
|
||||
baseModel,
|
||||
modelConfigService,
|
||||
availabilityService,
|
||||
router,
|
||||
authService,
|
||||
policyEngine,
|
||||
mcpClientManager,
|
||||
skillCatalog,
|
||||
baseToolCatalog,
|
||||
sessionService,
|
||||
}
|
||||
|
||||
agentService = new AdkAgentService(shared)
|
||||
|
||||
function createSession(definition, parentSessionId?) {
|
||||
sessionRecord = sessionService.create({ definition, parentSessionId })
|
||||
registries = buildScopedRegistries(definition, parentSessionId)
|
||||
pipeline = createPipeline({
|
||||
sessionId: sessionRecord.id,
|
||||
agentId: definition.name,
|
||||
parentSessionId,
|
||||
})
|
||||
|
||||
model = new GcliAgentModel(baseModel, pipeline)
|
||||
runtime = createAdkRuntime({
|
||||
definition,
|
||||
model,
|
||||
sessionRecord,
|
||||
registries,
|
||||
policyEngine,
|
||||
})
|
||||
|
||||
return new AgentSession(new AdkAgentProtocolAdapter(runtime))
|
||||
}
|
||||
|
||||
function createSubagentTool(definition) {
|
||||
return new FunctionTool(async (input, toolContext) => {
|
||||
child = createSession(definition, toolContext.sessionId)
|
||||
stream = await child.send(userMessage(input))
|
||||
|
||||
for await (event of child.stream(stream)) {
|
||||
projectChildEventToParent(toolContext, event)
|
||||
}
|
||||
|
||||
return collectFinalText()
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Child session projection shape:
|
||||
|
||||
```typescript
|
||||
emit(tool_request({ requestId, name: definition.name, args: input }))
|
||||
|
||||
for await (event of childSession.stream({ streamId })) {
|
||||
if (event.type === 'message' || event.type === 'tool_update') {
|
||||
emit(
|
||||
tool_update({
|
||||
requestId,
|
||||
content: projectDisplayContent(event),
|
||||
}),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if (event.type === 'error') {
|
||||
emit(
|
||||
tool_response({
|
||||
requestId,
|
||||
name: definition.name,
|
||||
isError: true,
|
||||
content: projectErrorContent(event),
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === 'agent_end') {
|
||||
emit(
|
||||
tool_response({
|
||||
requestId,
|
||||
name: definition.name,
|
||||
content: collectFinalChildResult(),
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Only the final `tool_response` is returned to the parent model. `tool_update` remains a progress/UI projection surface.
|
||||
|
||||
Service lifetime split:
|
||||
|
||||
- shared across sessions: model config, availability, routing, auth, policy engine, MCP manager, skill catalog
|
||||
- scoped per agent/session: `AgentSession`, pipeline instance, tool/prompt/resource registries, derived message bus
|
||||
- scoped per invocation: shell process, MCP request, confirmation continuation, tool progress updates
|
||||
|
||||
---
|
||||
|
||||
# Status of this document approvals table {#status-of-this-document}
|
||||
|
||||
| #begin-approvals-addon-section See [go/g3a-approvals](http://goto.google.com/g3a-approvals) for instructions on adding reviewers. |
|
||||
| :---: |
|
||||
.
|
||||
@@ -0,0 +1,554 @@
|
||||
# Unified ADK CLI Design Review
|
||||
|
||||
Date: 2026-04-06
|
||||
|
||||
> **Rollout:** All ADK migration work is feature-gated behind `experimental.adk` flags. No behavioral changes ship without an explicit opt-in. This applies to both non-interactive and interactive flows.
|
||||
|
||||
Inputs merged:
|
||||
- `claude-adk-cli-design-review.md` (adversarial code-level review)
|
||||
- `codex-adk-cli-design-review.md`
|
||||
- Follow-up review discussion on subagent architecture, session semantics, and `BaseContextCompactor`
|
||||
|
||||
Scope reviewed:
|
||||
- `gemini-cli/docs/adk-replat/adk_migration_design_doc.md`
|
||||
- `gemini-cli` `AgentProtocol` / `AgentSession` / interactive migration work
|
||||
- `adk-js` runner / session / tool / security / model architecture
|
||||
- PR context for interactive migration, including `google-gemini/gemini-cli#24297`
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The design is directionally sound and the migration goal is correct. However, the document is not yet principal-review ready due to:
|
||||
|
||||
1. **Four non-compilable code samples** (C1-C4) that will immediately erode reviewer confidence
|
||||
2. **An under-specified translation boundary** — the ADK→AgentProtocol mapping is the real architecture, but the doc treats it as an afterthought
|
||||
3. **Overclaimed parity** in routing, telemetry, and approvals
|
||||
4. **Missing phased plan** with clear milestones and non-goals
|
||||
|
||||
The central architectural insight is right: the migration boundary is:
|
||||
|
||||
- `adk-js Event -> Gemini CLI AgentEvent`
|
||||
- `adk-js session/runtime semantics -> AgentProtocol / AgentSession semantics`
|
||||
|
||||
That boundary carries stream lifecycle, replay/resume, event projection, approval correlation, and persistence correctness. Until the translation architecture is explicit, parity claims remain unsupported.
|
||||
|
||||
## Compilation Blockers (Must Fix First)
|
||||
|
||||
These are binary errors — the code samples in the design doc will not compile against current ADK types.
|
||||
|
||||
### C1. `LlmRequest` has no `headers` field
|
||||
|
||||
`adk-js/core/src/models/llm_request.ts` defines: `model?`, `contents`, `config?`, `liveConnectConfig`, `toolsDict`. The design doc's `request.headers = {...}` will silently fail or not compile. **Fix:** use `llmRequest.config.httpOptions.headers` or pass headers at `Gemini` constructor time.
|
||||
|
||||
### C2. `BaseLlm.connect()` is abstract and unimplemented
|
||||
|
||||
`adk-js/core/src/models/base_llm.ts:67-78` has TWO abstract methods: `generateContentAsync` and `connect()`. `GcliAgentModel` must implement both. **Fix:** add `connect()` — stub with `throw new Error('not supported')` if live connections are out of scope.
|
||||
|
||||
### C3. `BaseToolset` constructor signature is wrong
|
||||
|
||||
`adk-js/core/src/tools/base_toolset.ts:46-49`: `constructor(readonly toolFilter: ToolPredicate | string[], readonly prefix?: string)`. The design doc's `super([])` passes an empty array as `toolFilter`, which makes `isToolSelected` return false for all tools — a silent bug. Also `close()` is abstract and must be implemented. **Fix:** match the actual signature.
|
||||
|
||||
### C4. `FunctionalTool` does not exist
|
||||
|
||||
The design doc references `FunctionalTool` for subagent wrapping. This class does not exist in adk-js. The correct classes are `FunctionTool` (wraps a plain function) and `AgentTool` (wraps an agent with isolated runner). See subagent architecture section below for the recommended approach.
|
||||
|
||||
---
|
||||
|
||||
## High-Risk Findings
|
||||
|
||||
### 1. The actual migration boundary is under-specified
|
||||
|
||||
Current `AgentProtocol` requires `send()` timing guarantees, replay/reattach semantics, `streamId`, and optional `threadId` for subagent threads in `gemini-cli/packages/core/src/agent/types.ts` and `gemini-cli/packages/core/src/agent/agent-session.ts`.
|
||||
|
||||
ADK emits a different event model in `adk-js/core/src/events/event.ts`.
|
||||
|
||||
The doc needs a first-class architecture section for:
|
||||
- `AdkSessionRuntime`
|
||||
- `AdkEventTranslator`
|
||||
- event buffering / replay
|
||||
- `threadId` mapping
|
||||
- `tool_update` mapping
|
||||
- `agent_start` / `agent_end` ownership
|
||||
|
||||
Without that, the rest of the design sits on an unstated core contract.
|
||||
|
||||
### 2. The consolidated decorator needs two concerns extracted
|
||||
|
||||
The `GcliAgentModel.generateContentAsync` pipeline is a reasonable orchestrator pattern — each concern is a separate injected service call, not a monolithic class. However, two concerns don't belong in the model layer:
|
||||
|
||||
- **Quota/availability prompting** — involves interactive UI suspension, not request mutation. Should use `beforeModelCallback` on `LlmAgent`.
|
||||
- **Policy/approval control flow** — involves blocking for user input. Should use `beforeToolCallback` or the `ApprovalBridge` (see architecture section).
|
||||
|
||||
The remaining concerns (auth header injection, routing/model rewrite, compaction, masking) are legitimate request-preprocessing and can stay in the model pipeline.
|
||||
|
||||
### 3. The design frequently conflates “possible with custom logic” with “supported by current ADK architecture”
|
||||
|
||||
This is one of the main wording risks.
|
||||
|
||||
Several statements currently read like native ADK parity when the real meaning is:
|
||||
- possible with substantial Gemini CLI-owned bridge logic
|
||||
- possible by bypassing native ADK facilities
|
||||
- possible only for phase 1 with intentionally degraded semantics
|
||||
|
||||
That distinction needs to be explicit everywhere the doc uses strong language like “validated” or “100% possible today.”
|
||||
|
||||
### 4. Auth architecture is deeper than header injection — Coast Assist requires a custom transport
|
||||
|
||||
The auth section has two distinct problems:
|
||||
|
||||
**Problem A: API surface mismatch (C1).** `LlmRequest` has no `headers` field. Per-request headers are possible via `llmRequest.config.httpOptions.headers`, and `Gemini` constructor accepts a `headers` param (`google_llm.ts:57,79`). This is fixable.
|
||||
|
||||
**Problem B: Coast Assist / `LOGIN_WITH_GOOGLE` uses a different backend entirely.** The current `CodeAssistServer` (`packages/core/src/code_assist/server.ts:407`) does NOT call the standard Gemini API. It uses `google-auth-library`'s `AuthClient.request()` to talk to a Code Assist backend endpoint. The OAuth flow (`packages/core/src/code_assist/oauth2.ts`) handles browser launch (L305), local callback server (L492-615), token exchange (L546-550), credential caching (L739-750), and automatic token refresh via `OAuth2Client`.
|
||||
|
||||
This means `GcliAgentModel` cannot wrap a standard `Gemini` BaseLlm and just inject headers. For Coast Assist auth, `GcliAgentModel` must **extend `BaseLlm` directly** and implement its own HTTP transport using `AuthClient.request()`, translating between `LlmRequest`/`LlmResponse` and the Code Assist protocol.
|
||||
|
||||
What works:
|
||||
- The OAuth dance itself (browser launch, token caching, refresh) runs before the agent session starts — no blocking inside `generateContentAsync`
|
||||
- `OAuth2Client.getAccessToken()` handles mid-session token refresh transparently
|
||||
- Per-request header injection works for API-key-based auth via `httpOptions.headers`
|
||||
|
||||
What the design doc must change:
|
||||
- Stop assuming `Gemini` as the inner model — `GcliAgentModel extends BaseLlm` directly
|
||||
- Define two transport paths: standard Gemini API (API key) and Code Assist backend (OAuth)
|
||||
- The dummy-key workaround is irrelevant for Coast Assist — you never call `GoogleGenAI.models.generateContent`
|
||||
|
||||
### 5. Approval / elicitation bridging is acceptable only as a temporary non-native bridge
|
||||
|
||||
The design’s callback-based approval approach is pragmatic for phase 1, but it bypasses native ADK confirmation semantics.
|
||||
|
||||
`adk-js/core/src/plugins/security_plugin.ts` and `adk-js/core/src/agents/processors/request_confirmation_llm_request_processor.ts` show that ADK’s model is:
|
||||
- `ALLOW | DENY | CONFIRM`
|
||||
- persisted confirmation state
|
||||
- later resumption from history
|
||||
|
||||
Blocking in callbacks may be acceptable for the first rollout, but the doc must say clearly:
|
||||
- local-only bridge
|
||||
- not resumable across process death
|
||||
- not long-term SDK behavior
|
||||
- intentionally non-native pending future elicitation/bidi work
|
||||
|
||||
### 6. Rewind is deeper than file truncation
|
||||
|
||||
The doc treats rewind as storage truncation. The storage part is straightforward — `GcliFileSessionService` wraps the existing `ChatRecordingService` and implements 4 abstract methods (`createSession`, `getSession`, `listSessions`, `deleteSession`). DB-level concerns (locking, stale-writer detection, `PESSIMISTIC_WRITE`) do not apply to a single-user CLI.
|
||||
|
||||
However, rewind itself is not just storage:
|
||||
- `adk-js/core/src/agents/processors/request_confirmation_llm_request_processor.ts` scans event history to reconstruct pending confirmations and resume tool execution
|
||||
- `adk-js/core/src/runner/runner.ts:447` has a TODO acknowledging the event log is used as a transaction log
|
||||
- Truncating events without rolling back derived state (pending confirmations, app/user state prefixes) will break resumption
|
||||
|
||||
The doc should define rewind as: event-log truncation + session state recomputation + confirmation/auth state rollback.
|
||||
|
||||
Note: `BaseSessionService.appendEvent` handles state merging with `app:`, `user:`, `temp:` prefixed keys automatically — this is inherited for free.
|
||||
|
||||
### 7. Routing, telemetry, and plan mode are all overclaimed
|
||||
|
||||
Routing:
|
||||
- current routing requires a richer `RoutingContext`
|
||||
- `contents.slice(0, -1)` / `contents.pop()` is not a sufficient or type-accurate mapping
|
||||
- model banning via simulated error is not the same as model routing
|
||||
|
||||
Telemetry:
|
||||
- **The stream-interceptor approach in section 3.6 is wrong.** The correct mechanism is ADK's `BasePlugin` system.
|
||||
- `event.id` is not a safe per-tool correlation key — use `functionCall.id` via `beforeToolCallback`/`afterToolCallback`
|
||||
- ADK can batch multiple function calls or responses in one event — stream interception cannot distinguish them, but plugin hooks fire per-tool with distinct `functionCallId`
|
||||
- **Correction:** Token usage IS available on ADK events. `llm_agent.ts:831-834` spreads `LlmResponse` (including `usageMetadata`) into the event via `createEvent({...modelResponseEvent, ...llmResponse})`. Additionally, `afterModelCallback` receives the raw `LlmResponse` with `usageMetadata` directly — providing a second capture point.
|
||||
- HTTP-level metrics (status code, request duration) are NOT on ADK events — must be captured in the `GcliAgentModel` wrapper
|
||||
- The sample APIs in the design do not match the current `ClearcutLogger` taxonomy (~195 distinct metadata keys)
|
||||
|
||||
**Recommended telemetry architecture:**
|
||||
|
||||
| Metric Category | Capture Mechanism | ADK Hook |
|
||||
|---|---|---|
|
||||
| Token counts (input, output, cached, thinking) | `afterModelCallback` → `LlmResponse.usageMetadata` | `BasePlugin.afterModelCallback` |
|
||||
| Per-tool timing | Timer keyed on `functionCallId` | `BasePlugin.beforeToolCallback` / `afterToolCallback` |
|
||||
| Agent lifecycle | Start/end tracking | `BasePlugin.beforeAgentCallback` / `afterAgentCallback` |
|
||||
| Model errors | Error classification | `BasePlugin.onModelErrorCallback` |
|
||||
| HTTP status, API duration | Captured in model wrapper | `GcliAgentModel.generateContentAsync` |
|
||||
| Routing decisions, latency | Captured in model or beforeModelCallback | `beforeModelCallback` / model wrapper |
|
||||
| Context token breakdowns (system, tools, history) | Computed in request pipeline | Model wrapper |
|
||||
| Tool approval decisions | Injected from approval layer | `ApprovalBridge` |
|
||||
| Session config, compression, rewind, IDE, extensions, billing, slash commands, hooks, plan execution, onboarding | **Unchanged** — stays in current call sites | Existing gemini-cli code |
|
||||
|
||||
~30% of Clearcut metrics map to ADK plugin hooks. ~70% remain in their current call sites unchanged.
|
||||
|
||||
Plan mode:
|
||||
- current behavior is broader than prompt + tool filtering
|
||||
- policy, approval mode, routing, and config refresh are all part of the behavior
|
||||
|
||||
### 8. Retry safety around side effects (note)
|
||||
|
||||
The “full turn reset” proposal could duplicate side effects (file writes, shell commands, MCP mutations) if the turn already executed side-effecting tools before the stream failure. This is an ADK-wide concern, not specific to this migration. A brief note acknowledging this limitation is sufficient — a full retry safety taxonomy is out of scope for this design.
|
||||
|
||||
### 9. Design ignores ADK's native `BaseContextCompactor`
|
||||
|
||||
ADK already has a compaction extension point: `BaseContextCompactor` (`adk-js/core/src/context/base_context_compactor.ts`) with `shouldCompact(invocationContext)` + `compact(invocationContext)`. It's wired into the request processor pipeline via `ContextCompactorRequestProcessor` at `llm_agent.ts:407-420`.
|
||||
|
||||
The design doc proposes running compaction inside `GcliAgentModel.generateContentAsync` instead. This works but bypasses the native slot. Recommendation: implement `BaseContextCompactor`, delegate to the existing `ChatCompressionService` internally. ADK provides the trigger point; your service provides the logic. Note that `BaseContextCompactor` returns void — failure states (COMPRESSED, NOOP, CONTENT_TRUNCATED, etc.) must be communicated via session state or custom events.
|
||||
|
||||
### 10. Existing `isStructuredError` type-guard bug
|
||||
|
||||
`gemini-cli/packages/core/src/agent/event-translator.ts:431-438`: `isStructuredError()` checks only `typeof error === 'object' && 'message' in error && typeof error.message === 'string'`. Since every `Error` instance has `message: string`, plain `Error` objects pass this guard. In `mapError` (lines 390-429), the structured branch runs before `instanceof Error`, so plain errors get incorrect HTTP→gRPC status mapping. This should be fixed before the translator becomes the long-term session boundary. **Fix:** add `'status' in error` to the guard, or check `instanceof Error` first.
|
||||
|
||||
### 11. The review should distinguish architecture defects from rollout-status evidence
|
||||
|
||||
The interactive PR findings matter:
|
||||
- stacked-branch dependency
|
||||
- hook-order risk in `#24297`
|
||||
- `_meta.legacyState` leakage into protocol events
|
||||
|
||||
But they are secondary to the core design defect, which is the missing runtime/translation architecture.
|
||||
|
||||
These findings should stay in the review, but as credibility and rollout-risk evidence rather than the centerpiece.
|
||||
|
||||
## Recommended Architecture
|
||||
|
||||
This is the cleanest merged recommendation from both reviews plus follow-up discussion.
|
||||
|
||||
### Core principle
|
||||
|
||||
Use one shared ADK-based execution/runtime core, then put different adapters on top of it.
|
||||
|
||||
Do not make `AgentSession` the innermost engine.
|
||||
|
||||
### Recommended split
|
||||
|
||||
1. `AdkRuntimeCore`
|
||||
- owns the ADK runner loop
|
||||
- owns tool execution, policy integration, routing integration, masking, compaction hooks, and persistence hooks
|
||||
- emits runtime-level activity/events
|
||||
|
||||
2. `TopLevelSessionAdapter`
|
||||
- exposes the runtime as `AgentProtocol` / `AgentSession`
|
||||
- owns replay, reattach, `streamId`, `threadId`, `agent_start` / `agent_end`, and top-level event projection
|
||||
|
||||
3. `SubagentTools` (custom `BaseTool` implementations)
|
||||
- subagents invoke the same shared runtime core as the main agent
|
||||
- the only difference is the type mapping at the boundary (projecting child activity into parent-facing `tool_update` / `tool_response` or `threadId`-scoped events)
|
||||
- `AgentTool` is explicitly rejected — it creates a fully isolated runner/session, which conflicts with the shared-core goal
|
||||
- custom `BaseTool` wrappers allow subagents to share policy, routing, tool definitions, and config with the parent while keeping the door open for future resumability and richer state sharing
|
||||
|
||||
4. `ApprovalBridge`
|
||||
- phase-1 callback bridge for approvals / elicitation using existing callbacks
|
||||
- clearly documented as temporary and non-native
|
||||
- eventual migration to full elicitation bidi format
|
||||
|
||||
5. `GcliFileSessionService`
|
||||
- file-backed persistence wrapping existing `ChatRecordingService`
|
||||
- implements 4 abstract methods from `BaseSessionService`
|
||||
- inherits state-merging (app/user/temp prefixes) for free
|
||||
- single-writer model — concurrent runs per session are forbidden
|
||||
|
||||
### Subagent architecture decision
|
||||
|
||||
`AgentTool` is not the right abstraction for this migration. It creates an isolated runner with its own `InMemorySessionService`, meaning:
|
||||
- state is NOT shared with the parent
|
||||
- events are NOT projected into the parent stream
|
||||
- policy, routing, and config are NOT inherited
|
||||
|
||||
The core requirement is that subagents use the same core loop functionality as the main agent — the only difference is how results are mapped back to the parent. Custom `BaseTool` implementations that invoke the shared `AdkRuntimeCore` achieve this. This also positions subagents for future complexity (resumability, richer state sharing, MCP-scoped tool sets) without fighting AgentTool's isolation model.
|
||||
|
||||
### Session conclusion
|
||||
|
||||
Main agent and subagents can share the same underlying runtime core.
|
||||
|
||||
But they should not necessarily share the exact same public adapter.
|
||||
|
||||
The right formulation is:
|
||||
- same core runtime
|
||||
- different event/session projections depending on embedding context
|
||||
|
||||
## Section-by-Section Unified Review
|
||||
|
||||
### 3.1 Authentication Flexibility
|
||||
|
||||
Assessment: implementable, but the design doc’s approach is wrong for Coast Assist.
|
||||
|
||||
Keep:
|
||||
- ADK `Gemini` does not natively accept the CLI’s auth shape
|
||||
|
||||
Change:
|
||||
- `GcliAgentModel` must extend `BaseLlm` directly, not wrap `Gemini` — Coast Assist uses `AuthClient.request()` against a non-standard backend, not `GoogleGenAI`
|
||||
- fix `request.headers` to `llmRequest.config.httpOptions.headers` (C1)
|
||||
- define two transport paths: API-key (standard Gemini) and OAuth (Code Assist)
|
||||
- token refresh is transparent via `OAuth2Client` — this works as-is
|
||||
- browser-based OAuth runs before agent session starts — no blocking concern
|
||||
|
||||
### 3.2 Model Steering and Mid-Stream Injection
|
||||
|
||||
Assessment: next-turn steering is plausible now; true live interruption is still blocked.
|
||||
|
||||
Required change:
|
||||
- split “next-step steering” from “true mid-turn interruption”
|
||||
- define temporary user-visible behavior until live input-stream support exists
|
||||
|
||||
### 3.3 State Management and Token Compaction
|
||||
|
||||
Assessment: plausible, but too wrapper-centric.
|
||||
|
||||
Required change:
|
||||
- specify whether compaction operates on persisted history, outgoing request history, or both
|
||||
- define recursion guards and utility-call isolation
|
||||
- define artifact ownership across sessions and subagents
|
||||
|
||||
### 3.4 Model Configuration and Hierarchical Overrides
|
||||
|
||||
Assessment: under-specified.
|
||||
|
||||
Required change:
|
||||
- model config resolution should remain request-scoped
|
||||
- preserve scoped overrides and retry-aware behavior
|
||||
- explain subagent and utility-call override behavior
|
||||
|
||||
### 3.5 Universal Policy Enforcement
|
||||
|
||||
Assessment: acceptable as a bridge, not as full parity.
|
||||
|
||||
Required change:
|
||||
- explicitly document reduced phase-1 semantics
|
||||
- list policy inputs lost unless extra context plumbing is added
|
||||
- separate short-term callback bridge from long-term native confirmation flow
|
||||
|
||||
### 3.6 Telemetry and Observability (Clearcut)
|
||||
|
||||
Assessment: fully implementable, but the stream-interceptor approach must be replaced with a `BasePlugin`.
|
||||
|
||||
Required change:
|
||||
- replace stream interception with `ClearcutTelemetryPlugin extends BasePlugin`
|
||||
- use `afterModelCallback` for token counts (`LlmResponse.usageMetadata` is available)
|
||||
- use `beforeToolCallback`/`afterToolCallback` with `functionCallId` for per-tool timing
|
||||
- capture HTTP-level metrics (status code, duration) in `GcliAgentModel` wrapper
|
||||
- ~70% of Clearcut metrics stay in their current call sites unchanged — document which
|
||||
- replace fake API examples with a telemetry parity matrix showing capture mechanism per metric
|
||||
|
||||
### 3.7 Dynamic Model Routing and Configurability
|
||||
|
||||
Assessment: partly feasible, materially overstated.
|
||||
|
||||
Required change:
|
||||
- remove “100% possible today”
|
||||
- separate alias rewrite, fallback selection, classifier routing, and banning/rejection behavior
|
||||
- define a real `RoutingContextBuilder`
|
||||
|
||||
### 3.8 Fallbacks and Availability Management
|
||||
|
||||
Assessment: incomplete.
|
||||
|
||||
Required change:
|
||||
- split preflight fallback from post-failure transition behavior
|
||||
- define retry safety
|
||||
- define main-call vs utility-call fallback ownership
|
||||
|
||||
### 3.9 State-Driven Mode Switching
|
||||
|
||||
Assessment: too narrow.
|
||||
|
||||
Required change:
|
||||
- define a single mode state source of truth
|
||||
- enumerate all consumers: prompt, toolset, policy, router, UI
|
||||
|
||||
### 3.10 Tool Output Masking
|
||||
|
||||
Assessment: one of the stronger sections.
|
||||
|
||||
Required change:
|
||||
- define whether masking mutates persisted history or only outgoing request history
|
||||
- define ordering and idempotence relative to compaction
|
||||
- define file/artifact lifecycle
|
||||
|
||||
### 4. SDK Facade / Stateful Orchestration
|
||||
|
||||
Assessment: this should become the architectural center of the document.
|
||||
|
||||
Required change:
|
||||
- expand ownership boundaries
|
||||
- describe the shared runtime plus adapter model
|
||||
- document temporary approval/elicitation limitations explicitly
|
||||
|
||||
### 4.1 Hybrid Tool Instantiation
|
||||
|
||||
Assessment: directionally good.
|
||||
|
||||
Required change:
|
||||
- include message bus derivation
|
||||
- include MCP discovery scoping
|
||||
- include recursion prevention and tool isolation details
|
||||
|
||||
### 4.2 Custom File-Based Persistence
|
||||
|
||||
Assessment: straightforward for storage, but rewind semantics need work.
|
||||
|
||||
The storage layer itself is low-risk — `GcliFileSessionService` wraps existing `ChatRecordingService` and implements 4 abstract methods. DB concerns (locking, stale-writer, crash recovery) don't apply to a single-user CLI.
|
||||
|
||||
Required change:
|
||||
- define rewind as event-log truncation + state recomputation (not just file truncation)
|
||||
- state that concurrent runs per session are forbidden (single-writer)
|
||||
- define how app/user/temp state prefixes map to existing workspace JSON
|
||||
|
||||
### 4.3 Decomposing `AgentLoopContext`
|
||||
|
||||
Assessment: right instinct, incomplete decomposition.
|
||||
|
||||
Required change:
|
||||
- account for message bus / confirmation routing
|
||||
- account for injection queues
|
||||
- account for prompt/resource registries and MCP scoping
|
||||
|
||||
### 5.1 Real-Time User Message Injections
|
||||
|
||||
Assessment: blocker analysis is incomplete.
|
||||
|
||||
Required change:
|
||||
- mention TS live runtime gaps directly
|
||||
- describe temporary UX until true interruption exists
|
||||
|
||||
### 5.2 Conversation Rewind and State Reversal
|
||||
|
||||
Assessment: too shallow today.
|
||||
|
||||
Required change:
|
||||
- define rewind as event-log truncation plus state rollback semantics
|
||||
- define confirmation/auth rollback semantics
|
||||
- define artifact/version rollback expectations
|
||||
|
||||
### 5.3 Concurrent Sessions
|
||||
|
||||
Assessment: a single-user CLI does not need DB-level locking.
|
||||
|
||||
Required change:
|
||||
- state that concurrent runs per session are forbidden (single-writer model)
|
||||
- this is sufficient for a CLI — no further locking design needed
|
||||
|
||||
## Corrections to Earlier Reviews
|
||||
|
||||
### Auth is implementable, not blocked
|
||||
API-key auth works via `httpOptions.headers`. Coast Assist / `LOGIN_WITH_GOOGLE` requires `GcliAgentModel` to extend `BaseLlm` directly with its own transport (using `AuthClient.request()`), not wrap `Gemini`. The OAuth flow, token refresh, and credential caching all work as-is — the issue was the design doc's assumption about the inner model, not auth capability.
|
||||
|
||||
### Persistence is simpler than initially claimed
|
||||
DB-level concerns (PESSIMISTIC_WRITE, stale-writer detection, concurrent append policy) come from `DatabaseSessionService` and don't apply. The file-backed service wraps existing `ChatRecordingService`. Only rewind semantics need deeper design.
|
||||
|
||||
### Interactive PR findings are rollout-risk evidence, not architecture defects
|
||||
The branch-stack dependency, hook-order risk, and `_meta.legacyState` leakage in PR #24297 are real but secondary to the core architecture gaps above.
|
||||
|
||||
## What To Change Before Principal Review
|
||||
|
||||
1. **Fix the 4 compilation blockers** (C1-C4) — these will be the first thing reviewers check
|
||||
2. **Add an explicit ADK→AgentProtocol translation architecture section** — this is the real design center
|
||||
3. **Add the shared-core subagent architecture** — custom `BaseTool` over shared runtime, not `AgentTool`
|
||||
4. **Use `BaseContextCompactor`** for compaction instead of embedding it in the model layer
|
||||
5. **Replace “100% possible today” and similar language** with explicit labels: native / bridgeable / blocked
|
||||
6. **Add a parity matrix** (feature | ADK mechanism | status | bridge workaround | long-term target)
|
||||
|
||||
## Phased Migration Plan
|
||||
|
||||
### Phase 0: Foundation (current state → near-term)
|
||||
**Goal:** Establish the shared runtime core and translation layer.
|
||||
|
||||
- [ ] Fix compilation blockers (C1-C4) in design doc code samples
|
||||
- [ ] Implement `AdkRuntimeCore` wrapping ADK `Runner` + `LlmAgent`
|
||||
- [ ] Implement `AdkEventTranslator` (ADK `Event` → gemini-cli `AgentEvent`)
|
||||
- [ ] Implement `GcliAgentModel extends BaseLlm` with dual transport (API-key via `httpOptions.headers`, Coast Assist via `AuthClient.request()`)
|
||||
- [ ] Implement `GcliFileSessionService extends BaseSessionService` wrapping `ChatRecordingService`
|
||||
- [ ] Implement `GcliContextCompactor implements BaseContextCompactor` wrapping `ChatCompressionService`
|
||||
- [ ] Wire behind `experimental.adk` feature gate
|
||||
|
||||
**Non-goals for Phase 0:**
|
||||
- No interactive flow changes
|
||||
- No subagent support
|
||||
- No live/bidi connections
|
||||
- No resumable approvals
|
||||
|
||||
**Exit criteria:** Non-interactive flow produces identical output behind feature gate.
|
||||
|
||||
### Phase 1: Non-Interactive Parity
|
||||
**Goal:** Feature-gated non-interactive flow matches legacy behavior.
|
||||
|
||||
- [ ] Tool output masking via `BaseLlmRequestProcessor`
|
||||
- [ ] Model routing via `beforeModelCallback` + `RoutingContextBuilder` adapter
|
||||
- [ ] Policy enforcement via `beforeToolCallback` using existing callbacks (temporary bridge)
|
||||
- [ ] Quota/availability handling via `beforeModelCallback`
|
||||
- [ ] `ClearcutTelemetryPlugin extends BasePlugin` for token counts (`afterModelCallback`), per-tool timing (`beforeToolCallback`/`afterToolCallback` with `functionCallId`), agent lifecycle, model errors
|
||||
- [ ] HTTP-level telemetry (status code, duration) captured in `GcliAgentModel` wrapper
|
||||
- [ ] Verify ~70% of existing Clearcut call sites work unchanged
|
||||
|
||||
**Non-goals for Phase 1:**
|
||||
- No interactive UI changes
|
||||
- No plan mode switching
|
||||
- No subagents
|
||||
- Approvals are callback-based, not resumable
|
||||
|
||||
**Exit criteria:** Non-interactive tests pass with `experimental.adk.enabled = true`. Legacy path remains default.
|
||||
|
||||
### Phase 2: Interactive Flow Migration
|
||||
**Goal:** Interactive flow uses the same ADK runtime behind `TopLevelSessionAdapter`.
|
||||
|
||||
- [ ] `TopLevelSessionAdapter` exposing runtime as `AgentProtocol` / `AgentSession`
|
||||
- [ ] Stream lifecycle (`agent_start` / `agent_end` / `streamId` ownership)
|
||||
- [ ] Replay/reattach semantics matching current `AgentSession.stream()` behavior
|
||||
- [ ] Plan mode via dynamic `BaseToolset` + `InstructionProvider` + mode state
|
||||
- [ ] `ApprovalBridge` for tool confirmations using existing UI callbacks
|
||||
- [ ] Elicitation via existing callbacks (not yet bidi)
|
||||
- [ ] `_meta.legacyState` bridge for UI rendering (documented as temporary)
|
||||
|
||||
**Non-goals for Phase 2:**
|
||||
- No live/bidi parity (`runLiveImpl` is still a stub in ADK TS)
|
||||
- No true mid-turn interruption (steering limited to once-per-LLM-call)
|
||||
- No resumable approvals across process death
|
||||
- No concurrent multi-stream sessions
|
||||
|
||||
**Exit criteria:** Interactive flow works behind feature gate. Legacy path remains default.
|
||||
|
||||
### Phase 3: Subagents and SDK Readiness
|
||||
**Goal:** Subagents share the core runtime. The architecture is SDK-reusable.
|
||||
|
||||
- [ ] Custom `BaseTool` subagent wrappers over shared `AdkRuntimeCore`
|
||||
- [ ] `threadId`-scoped event projection for child activity
|
||||
- [ ] Shared policy, routing, and config inheritance
|
||||
- [ ] Remove `_meta.legacyState` — replace with proper event/adapter separation
|
||||
- [ ] Migrate approval bridge to native ADK elicitation/bidi (when available)
|
||||
- [ ] Define SDK public API surface
|
||||
|
||||
**Non-goals for Phase 3:**
|
||||
- Full ADK Dev UI compatibility
|
||||
- Agent-to-agent transfer via ADK's native mechanism
|
||||
|
||||
**Exit criteria:** Subagent invocation works. Architecture is documented for SDK consumers.
|
||||
|
||||
## Positive Signals
|
||||
|
||||
- Feature-gating behind `experimental.adk` is the right rollout pattern
|
||||
- Tool output masking maps cleanly to request-preprocessing
|
||||
- Dynamic toolset + `InstructionProvider` for plan mode is a good ADK-native fit
|
||||
- The phased migration checklist with tracking issues shows good engineering discipline
|
||||
- The doc correctly identifies 3 known ADK gaps (sections 5.1-5.3)
|
||||
- `AgentProtocol` / `AgentSession` is the right consumer-facing boundary
|
||||
- The policy adapter pattern is directionally sound even with reduced phase-1 semantics
|
||||
|
||||
## Concern Decomposition
|
||||
|
||||
| Concern | Level | ADK Mechanism |
|
||||
|---|---|---|
|
||||
| Auth (API key) | Transport | `BaseLlm` constructor / `httpOptions.headers` |
|
||||
| Auth (Coast Assist) | Transport | `BaseLlm` direct — custom transport via `AuthClient.request()` |
|
||||
| Model rewrite | Transport | `BaseLlm.generateContentAsync` model override |
|
||||
| Model routing | Agent | `beforeModelCallback` + `RoutingContextBuilder` |
|
||||
| Token compaction | Agent | `BaseContextCompactor` (native ADK) |
|
||||
| Quota/availability | Agent | `beforeModelCallback` |
|
||||
| Tool masking | Agent | `BaseLlmRequestProcessor` |
|
||||
| Tool confirmations | Agent+Consumer | `beforeToolCallback` + existing callbacks (phase 1) |
|
||||
| Telemetry (lifecycle) | Agent | `ClearcutTelemetryPlugin` — `afterModelCallback`, `beforeToolCallback`/`afterToolCallback` |
|
||||
| Telemetry (HTTP) | Transport | `GcliAgentModel` wrapper (status code, duration) |
|
||||
| Telemetry (CLI) | Consumer | Existing call sites (~70% of Clearcut metrics, unchanged) |
|
||||
| UI rendering | Consumer | Event subscriber / adapter |
|
||||
| Subagent invocation | Agent | Custom `BaseTool` over shared runtime core |
|
||||
|
||||
## Final Verdict
|
||||
|
||||
The design is architecturally correct in its goal. To be principal-review ready:
|
||||
|
||||
1. Fix the 4 compilation blockers
|
||||
2. Add the ADK→AgentProtocol translation architecture as the design center
|
||||
3. Adopt native `BaseContextCompactor` instead of model-layer compaction
|
||||
4. Use custom `BaseTool` subagents over shared core (not `AgentTool`)
|
||||
5. Downgrade parity claims to match reality
|
||||
6. Add the phased plan with explicit non-goals per phase
|
||||
|
||||
The migration path is: shared runtime core → translation layer → adapters (top-level session, subagent tools, approval bridge). Each phase ships independently behind the feature gate.
|
||||
Reference in New Issue
Block a user