Skip to content

sdk: add spawn/release lifecycle hooks in TS and Python#477

Merged
willwashburn merged 4 commits intomainfrom
feature/sdk-spawn-release-lifecycle-hooks
Mar 4, 2026
Merged

sdk: add spawn/release lifecycle hooks in TS and Python#477
willwashburn merged 4 commits intomainfrom
feature/sdk-spawn-release-lifecycle-hooks

Conversation

@willwashburn
Copy link
Copy Markdown
Member

@willwashburn willwashburn commented Mar 4, 2026

Summary

  • add per-call spawn lifecycle hooks (onStart, onSuccess, onError) to the TypeScript AgentRelay facade (spawn, spawnPty, and shorthand spawners)
  • add per-call release lifecycle hooks to TypeScript Agent.release(...) while keeping release("reason") backward compatible
  • add matching Python lifecycle hooks (on_start, on_success, on_error) to SpawnOptions, shorthand spawners, and Agent.release(...)
  • add TypeScript and Python tests for success/error hook behavior
  • update SDK docs and READMEs with hook options and usage examples

Validation

  • cd packages/sdk && npm run check
  • cd packages/sdk && npx vitest run src/tests/orchestration-upgrades.test.ts
  • cd packages/sdk-py && uv run --with pytest --with pytest-asyncio pytest tests/test_relay_lifecycle_hooks.py

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds per-call spawn and release lifecycle hooks (onStart, onSuccess, onError) to both the TypeScript and Python SDKs. These hooks allow callers to observe the outcome of individual spawn / release calls without relying solely on relay-level event hooks. The changes maintain full backward compatibility (e.g., agent.release("reason") still works in both languages).

Changes:

  • TypeScript (relay.ts): New lifecycle context/hook interfaces, updated spawnPty, spawn, createSpawner, and makeAgent to fire onStart/onSuccess/onError hooks around each network call; new helper methods invokeLifecycleHook, resetAgentLifecycleState, normalizeReleaseOptions.
  • Python (relay.py): Same lifecycle hook semantics added to SpawnOptions, AgentSpawner.spawn(), AgentRelay.spawn(), and Agent.release(), with matching helper methods.
  • Tests & Docs: New TS and Python test cases for success/error hook paths, updated READMEs and reference docs with hook options.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/sdk/src/relay.ts Core hook implementation for TS SDK: new interfaces, updated spawn/release methods, helper utilities
packages/sdk/src/__tests__/orchestration-upgrades.test.ts New TS tests covering spawn and release hook success/error paths
packages/sdk/README.md Updated quick-start example to show spawn and release lifecycle hooks
packages/sdk-py/src/agent_relay/relay.py Core hook implementation for Python SDK: updated SpawnOptions, AgentSpawner, AgentRelay, and Agent.release()
packages/sdk-py/tests/test_relay_lifecycle_hooks.py New Python tests for spawn and release hook success/error paths
packages/sdk-py/README.md Updated quick-start example to show Python lifecycle hooks
docs/reference/sdk.mdx Updated TS reference: spawn options table with hook columns, updated Agent.release() signature
docs/reference/sdk-py.mdx Updated Python reference: spawn options table with hook columns, updated Agent.release() signature

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 318 to 323
self._relay._invoke_lifecycle_hook(
on_start,
context,
f'spawn("{agent_name}") on_start',
)
client = await self._relay._ensure_started()
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

In AgentSpawner.spawn(), on_start is invoked (lines 318-322) before _ensure_started() is called (line 323). This means:

  1. If _ensure_started() raises (e.g., broker fails to start), on_start will have already fired but on_error will never be called, leaving the caller with a started hook and no error notification.
  2. This is inconsistent with AgentRelay.spawn() (and the TypeScript implementation), where _ensure_started() / ensureStarted() is always called before any lifecycle hooks fire.

The fix is to move _ensure_started() to before the on_start invocation, matching both the AgentRelay.spawn() pattern and the TypeScript behavior.

Suggested change
self._relay._invoke_lifecycle_hook(
on_start,
context,
f'spawn("{agent_name}") on_start',
)
client = await self._relay._ensure_started()
client = await self._relay._ensure_started()
self._relay._invoke_lifecycle_hook(
on_start,
context,
f'spawn("{agent_name}") on_start',
)

Copilot uses AI. Check for mistakes.
Comment thread docs/reference/sdk.mdx
}): Promise<Message>;

release(reason?: string): Promise<void>;
release(reasonOrOptions?: string | ReleaseOptions): Promise<void>;
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

The release(reasonOrOptions?: string | ReleaseOptions) signature references ReleaseOptions, but the documentation does not describe the shape of this type (its reason, onStart, onSuccess, and onError fields). Users who only refer to the reference docs won't know how to use the hook-based overload. Consider adding a ReleaseOptions properties table similar to the spawn options table, or at least a brief inline description near the Agent section.

Copilot uses AI. Check for mistakes.
@willwashburn
Copy link
Copy Markdown
Member Author

Addressed both review findings:\n\n1) Python shorthand spawner ordering: moved ensure_started before on_start in AgentSpawner.spawn, so startup failures no longer emit on_start without a matching completion path. Added regression test: test_shorthand_spawn_does_not_fire_start_hook_if_broker_startup_fails.\n\n2) TS docs clarity: added a ReleaseOptions section in docs/reference/sdk.mdx documenting reason, onStart, onSuccess, and onError.\n\nAlso re-ran Python lifecycle hook tests (7 passed).

devin-ai-integration[bot]

This comment was marked as resolved.

khaliqgant
khaliqgant previously approved these changes Mar 4, 2026
@willwashburn
Copy link
Copy Markdown
Member Author

Addressed the latest lifecycle-hook comment about release startup failures:\n\n- Moved ensureStarted/_ensure_started before onStart in both TS and Python release paths, so startup failures do not emit onStart without completion hooks.\n- Added regression tests in both SDKs verifying no release lifecycle hooks fire when startup fails before release begins.\n\nValidation:\n- cd packages/sdk && npx vitest run src/tests/orchestration-upgrades.test.ts (33 passed)\n- cd packages/sdk-py && uv run --with pytest --with pytest-asyncio pytest tests/test_relay_lifecycle_hooks.py (8 passed)

@willwashburn willwashburn merged commit 91e86aa into main Mar 4, 2026
32 checks passed
@willwashburn willwashburn deleted the feature/sdk-spawn-release-lifecycle-hooks branch March 4, 2026 06:42
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/sdk/src/relay.ts
Comment on lines +1064 to +1071
async release(reasonOrOptions?: string | ReleaseOptions) {
const releaseOptions = relay.normalizeReleaseOptions(reasonOrOptions);
const releaseContext: ReleaseLifecycleContext = {
name,
reason: releaseOptions.reason,
};
const client = await relay.ensureStarted();
await client.release(name, reason);
await relay.invokeLifecycleHook(releaseOptions.onStart, releaseContext, `release("${name}") onStart`);
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

In Agent.release, onStart fires on line 1070 before ensureStarted() is called on line 1071. If ensureStarted() throws (e.g., broker connection lost), the error propagates without calling onError. This creates an inconsistency: onStart fired, signaling that the release process has begun, but onError is skipped even though the operation failed.

To fix this, either:

  • Move ensureStarted() before the onStart call (matching the spawn behavior, where the broker is started before any hooks fire), or
  • Wrap the ensureStarted() call inside the existing try/catch block so onError is also invoked for connection failures.

Copilot uses AI. Check for mistakes.
Comment on lines +110 to +119
context = {
"name": self._name,
"reason": reason,
}
client = await self._relay._ensure_started()
await client.release(self._name, reason)
await self._relay._invoke_lifecycle_hook(
on_start,
context,
f'release("{self._name}") on_start',
)
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

In Agent.release, on_start fires on line 114 before _ensure_started() is called on line 119. If _ensure_started() throws (e.g., broker connection lost), the error propagates without invoking on_error. This creates an inconsistency where on_start fires but on_error is skipped even though the operation failed.

To fix this, either:

  • Move _ensure_started() before the on_start call (matching the spawn behavior where the broker is started before any hooks fire), or
  • Wrap the _ensure_started() call inside the existing try/except block so on_error is also invoked for connection failures.

Copilot uses AI. Check for mistakes.
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.

3 participants