Skip to content

Feat: Replace Promise<void> with proper Task type in ActiveStreamingTool Staging#468

Open
AmaadMartin wants to merge 1 commit into
google:mainfrom
AmaadMartin:feat/streaming-tool-task-type
Open

Feat: Replace Promise<void> with proper Task type in ActiveStreamingTool Staging#468
AmaadMartin wants to merge 1 commit into
google:mainfrom
AmaadMartin:feat/streaming-tool-task-type

Conversation

@AmaadMartin

Copy link
Copy Markdown
Collaborator

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

  • Closes: #issue_number
  • Related: #issue_number

2. Or, if no issue exists, describe the change:

Problem:
ActiveStreamingTool.task was typed as Promise<void>, which didn't allow for runtime control (cancellation, status check) like the Python reference implementation which uses asyncio.Task.

Solution:
Implemented a concrete Task class in core/src/utils/task.ts that wraps a promise and supports cancel() and done(). Updated ActiveStreamingTool to use this Task type. Updated exports in common.ts and index.ts.

Testing Plan

Please describe the tests that you ran to verify your changes. This is required
for all PRs that are not small documentation or typo fixes.

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Passed vitest results for:

  • core/test/utils/task_test.ts
  • core/test/agents/active_streaming_tool_test.ts

Manual End-to-End (E2E) Tests:

Updated tests/e2e/streaming/task_e2e_test.ts to simulate a streaming tool task using the new Task class and LiveRequestQueue, and verified it runs and passes.

  • I have manually tested my changes end-to-end.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

None.

@kalenkevich kalenkevich left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What is the purpose of that Task object?

@AmaadMartin

AmaadMartin commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author

What is the purpose of that Task object?

The main purpose is to get parity with ADK Python. A Promise object can't be cancelled once created, and also doesn't allow for synchronous status checks. Both of these are supported by the asyncio.Task type which ADK Python uses. The Task object in this PR supports both of those usecases for parity.

@kalenkevich

Copy link
Copy Markdown
Collaborator

What is the purpose of that Task object?

The main purpose is to get parity with ADK Python. A Promise object can't be cancelled once created, and also doesn't allow for synchronous status checks. Both of these are supported by the asyncio.Task type which ADK Python uses. The Task object in this PR supports both of those usecases for parity.

Can you please show an example of the real usage?

@AmaadMartin AmaadMartin force-pushed the feat/streaming-tool-task-type branch from 7644c40 to 9b215ba Compare July 1, 2026 20:37
@AmaadMartin

Copy link
Copy Markdown
Collaborator Author

Example 1: A Tool that Runs Forever (Continuous Monitoring)

Scenario: A tool that streams real-time data continuously, such as monitoring stock prices, CPU usage, or a live camera feed. It uses an infinite loop to poll for updates.

// TS representation of a tool that runs forever
async function* monitorStockPrice(symbol: string) {
  yield `Started monitoring ${symbol}`;
  while (true) { // Infinite loop: Will never exit on its own
    const price = await fetchPrice(symbol);
    yield `Price update for ${symbol}: $${price}`;
    await sleep(5000); // Polls every 5 seconds
  }
}
  • Without Task: The runner cannot break this loop. The tool will keep running in the background indefinitely, leaking memory and wasting API calls.
  • With Task: The runner calls task.cancel(), which triggers the cancellation (e.g., rejecting the pending sleep promise) and breaks the loop.

Example 2: A Tool that Doesn't Run Forever, but Needs to be Stopped (Long-Running Task)

Scenario: A tool that performs a heavy computation, downloads a large file, or runs a complex database query. It will eventually finish, but it takes a significant amount of time (e.g., several minutes).

// TS representation of a long-running tool that eventually finishes
async function* downloadLargeFile(url: string, destination: string, abortSignal: AbortSignal) {
  const response = await fetch(url, { signal: abortSignal });
  const reader = response.body.getReader();
  while (true) {
    const { done, value } = await reader.read();
    if (done) break; 
    await writeToDisk(destination, value);
    yield `Download progress: ${value.length} bytes written`;
    
    if (abortSignal.aborted) {
      throw new Error("Download cancelled");
    }
  }
}
  • Without Task: Even if the user cancels the request or the agent no longer needs the file, the promise will continue executing and downloading in the background until it completes.
  • With Task: The Task can wrap the abortSignal.abort() call. Calling task.cancel() immediately stops the download and frees up resources.

@AmaadMartin AmaadMartin force-pushed the feat/streaming-tool-task-type branch from 9b215ba to 20122f8 Compare July 6, 2026 16:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants