Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/blue-pears-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@rozenite/storage-plugin': minor
'@rozenite/vite-plugin': minor
'rozenite': minor
---

Add plugin `./sdk` entrypoints for typed agent tool descriptors backed by the
same tool contracts used at runtime.

The storage plugin now ships `@rozenite/storage-plugin/sdk` with typed
`storageTools` descriptors and shared tool contract exports, and the Rozenite
build pipeline now bundles per-target SDK declarations so plugin SDK entrypoints
publish clean `dist/sdk/index.d.ts` files.
15 changes: 15 additions & 0 deletions .changeset/eight-lobsters-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@rozenite/agent-sdk': minor
'rozenite': minor
---

Add the first public Agent SDK for programmatic Rozenite agent workflows.

The SDK now exposes `createAgentClient()` with `client.withSession(...)`,
`client.openSession()`, and `client.attachSession()` for session-scoped work,
plus `session.domains.*` and `session.tools.*` helpers for dynamic or
descriptor-based tool calls.

A new `@rozenite/agent-sdk/transport` subpath exposes the low-level HTTP
transport used by the CLI, and the docs and packaged skills now include a
dedicated `rozenite-agent-sdk` workflow.
13 changes: 13 additions & 0 deletions .changeset/green-bulldogs-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@rozenite/agent-bridge': minor
'@rozenite/agent-shared': minor
---

Add typed agent tool contracts and descriptors that can be shared across runtime
tool registration and SDK-facing plugin exports.

`@rozenite/agent-shared` now exposes `defineAgentToolContract(...)`,
`defineAgentToolDescriptor(...)`, and `defineAgentToolDescriptors(...)`, while
`@rozenite/agent-bridge` can infer handler input and result types from typed
tool contracts passed to `useRozeniteInAppAgentTool(...)` and
`useRozenitePluginAgentTool(...)`.
6 changes: 6 additions & 0 deletions .changeset/shiny-plums-spark.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rozenite/middleware': patch
'@rozenite/plugin-bridge': patch
---

Fix agent session startup so `createSession()` waits for mounted plugin registrations to settle before returning, reducing races when calling plugin tools immediately after session creation.
5 changes: 5 additions & 0 deletions .changeset/warm-kings-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rozenite/agent-sdk': patch
---

Fix agent SDK tool helpers so paginated results honor `maxItems` from the first page and `domains.callTool()` can be typed with explicit argument and result generics.
6 changes: 4 additions & 2 deletions apps/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
"main": "./src/main.tsx",
"private": true,
"scripts": {
"start": "expo start",
"start": "WITH_ROZENITE=true expo start",
"reset-project": "node ./scripts/reset-project.js",
"android": "expo run:android",
"ios": "expo run:ios",
"ios": "WITH_ROZENITE=true expo run:ios",
"web": "expo start --web",
"web:webpack": "webpack serve --config webpack.config.js --mode development",
"web:webpack:build": "webpack --config webpack.config.js --mode production",
Expand All @@ -23,6 +23,7 @@
"@react-navigation/native": "^7.1.17",
"@react-navigation/native-stack": "^7.3.21",
"@reduxjs/toolkit": "^2.8.2",
"@rozenite/agent-sdk": "workspace:*",
"@rozenite/agent-bridge": "workspace:*",
"@rozenite/controls-plugin": "workspace:*",
"@rozenite/expo-atlas-plugin": "workspace:*",
Expand All @@ -32,6 +33,7 @@
"@rozenite/network-activity-plugin": "workspace:*",
"@rozenite/overlay-plugin": "workspace:*",
"@rozenite/performance-monitor-plugin": "workspace:*",
"@rozenite/plugin-bridge": "workspace:*",
"@rozenite/react-navigation-plugin": "workspace:*",
"@rozenite/redux-devtools-plugin": "workspace:*",
"@rozenite/require-profiler-plugin": "workspace:*",
Expand Down
26 changes: 26 additions & 0 deletions apps/playground/scripts/advanced.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createAgentClient } from '@rozenite/agent-sdk';
import { storageTools } from '@rozenite/storage-plugin/sdk';

async function readUsernameFromStorage() {
const client = createAgentClient();

return await client.withSession(async (session) => {
await session.tools.call({
domain: '@rozenite/react-navigation-plugin',
tool: 'navigate',
args: { name: 'StoragePlugin' },
});

const result = await session.tools.call(storageTools.readEntry, {
adapterId: 'mmkv',
storageId: 'user-storage',
key: 'username',
});

return {
username: result.entry.value,
};
});
}

readUsernameFromStorage().then(console.log).catch(console.error);
29 changes: 18 additions & 11 deletions apps/playground/src/app/useAgentPlaygroundTools.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Alert } from 'react-native';
import { useRozeniteInAppAgentTool, type AgentTool } from '@rozenite/agent-bridge';
import { useRozeniteInAppAgentTool } from '@rozenite/agent-bridge';
import { defineAgentToolContract } from '@rozenite/agent-shared';

type ShowAlertInput = {
title?: string;
Expand All @@ -11,7 +12,7 @@ type RandomNumberInput = {
max?: number;
};

const showAlertTool: AgentTool = {
const showAlertTool = defineAgentToolContract<ShowAlertInput, { ok: true }>({
name: 'show-alert',
description: 'Show a native alert in the playground app.',
inputSchema: {
Expand All @@ -27,9 +28,12 @@ const showAlertTool: AgentTool = {
},
},
},
};
});

const randomNumberTool: AgentTool = {
const randomNumberTool = defineAgentToolContract<
RandomNumberInput,
{ min: number; max: number; value: number }
>({
name: 'random-number',
description: 'Return a random number in the optional [min, max] range.',
inputSchema: {
Expand All @@ -45,9 +49,12 @@ const randomNumberTool: AgentTool = {
},
},
},
};
});

const echoPayloadTool: AgentTool = {
const echoPayloadTool = defineAgentToolContract<
{ payload?: unknown },
{ echoed: unknown }
>({
name: 'echo-payload',
description: 'Echo back payload for quick tool call smoke tests.',
inputSchema: {
Expand All @@ -58,21 +65,21 @@ const echoPayloadTool: AgentTool = {
},
},
},
};
});

export const useAgentPlaygroundTools = () => {
useRozeniteInAppAgentTool<ShowAlertInput>({
useRozeniteInAppAgentTool({
tool: showAlertTool,
handler: ({ title, message }) => {
Alert.alert(title || 'Agent Playground', message || 'Alert from Agent tool.');

return {
ok: true,
ok: true as const,
};
},
});

useRozeniteInAppAgentTool<RandomNumberInput>({
useRozeniteInAppAgentTool({
tool: randomNumberTool,
handler: ({ min = 0, max = 100 }) => {
const safeMin = Number.isFinite(min) ? min : 0;
Expand All @@ -89,7 +96,7 @@ export const useAgentPlaygroundTools = () => {
},
});

useRozeniteInAppAgentTool<{ payload?: unknown }>({
useRozeniteInAppAgentTool({
tool: echoPayloadTool,
handler: ({ payload }) => {
return {
Expand Down
3 changes: 2 additions & 1 deletion commitlint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ export default {
'controls-plugin',
'agent-bridge',
'agent-shared',
'agent-sdk',
'file-system-plugin',
'sqlite-plugin',
''
'',
],
],
},
Expand Down
1 change: 1 addition & 0 deletions commitlint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default {
'controls-plugin',
'agent-bridge',
'agent-shared',
'agent-sdk',
'file-system-plugin',
'sqlite-plugin',
'',
Expand Down
48 changes: 46 additions & 2 deletions packages/agent-bridge/src/useRozeniteAgentTool.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,21 @@

import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { defineAgentToolContract } from '@rozenite/agent-shared';
import {
afterEach,
beforeEach,
describe,
expect,
expectTypeOf,
it,
vi,
} from 'vitest';
import type { AgentTool } from './types.js';
import { useRozeniteInAppAgentTool } from './useRozeniteAgentTool.js';
import {
useRozeniteInAppAgentTool,
useRozenitePluginAgentTool,
} from './useRozeniteAgentTool.js';

declare global {
var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
Expand Down Expand Up @@ -61,6 +73,21 @@ const TOOL: AgentTool = {
},
};

const TYPED_TOOL = defineAgentToolContract<
{ key: string },
{ value: string }
>({
name: 'read-entry',
description: 'Read a single storage entry value by key.',
inputSchema: {
type: 'object',
properties: {
key: { type: 'string' },
},
required: ['key'],
},
});

function TestComponent({ enabled = true }: { enabled?: boolean }) {
useRozeniteInAppAgentTool({
tool: TOOL,
Expand All @@ -71,6 +98,19 @@ function TestComponent({ enabled = true }: { enabled?: boolean }) {
return null;
}

const assertToolContractInference = () => {
useRozenitePluginAgentTool({
pluginId: '@rozenite/storage-plugin',
tool: TYPED_TOOL,
handler: async (args) => {
expectTypeOf(args).toEqualTypeOf<{ key: string }>();
return {
value: args.key,
};
},
});
};

const renderTool = async (
enabled = true,
): Promise<{
Expand Down Expand Up @@ -165,4 +205,8 @@ describe('useRozeniteAgentTool', () => {

await unmountTool(root, container);
});

it('infers handler input and output from typed tool contracts', () => {
expectTypeOf(assertToolContractInference).toBeFunction();
});
});
57 changes: 33 additions & 24 deletions packages/agent-bridge/src/useRozeniteAgentTool.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { useEffect, useRef } from 'react';
import type {
InferAgentToolArgs,
InferAgentToolResult,
} from '@rozenite/agent-shared';
import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge';
import {
AGENT_PLUGIN_ID,
Expand All @@ -19,35 +23,38 @@ type AgentEventMap = {
};

export interface UseRozeniteAgentToolOptions<
TInput = unknown,
TOutput = unknown,
TTool extends AgentTool = AgentTool,
> {
tool: AgentTool;
handler: (args: TInput) => Promise<TOutput> | TOutput;
tool: TTool;
handler: (
args: InferAgentToolArgs<TTool>,
) => Promise<InferAgentToolResult<TTool>> | InferAgentToolResult<TTool>;
enabled?: boolean;
}

export interface UseRozenitePluginAgentToolOptions<
TInput = unknown,
TOutput = unknown,
> extends UseRozeniteAgentToolOptions<TInput, TOutput> {
export type UseRozenitePluginAgentToolWithPluginIdOptions<
TTool extends AgentTool = AgentTool,
> = {
pluginId: string;
}
} & UseRozeniteAgentToolOptions<TTool>;

export type UseRozenitePluginAgentToolOptions<
TTool extends AgentTool = AgentTool,
> = UseRozenitePluginAgentToolWithPluginIdOptions<TTool>;

export type UseRozeniteInAppAgentToolOptions<
TInput = unknown,
TOutput = unknown,
> = UseRozeniteAgentToolOptions<TInput, TOutput>;
TTool extends AgentTool = AgentTool,
> = UseRozeniteAgentToolOptions<TTool>;

const APP_DOMAIN = 'app';

const getQualifiedToolName = (domain: string, toolName: string): string => {
return `${domain.trim()}.${toolName.trim()}`;
};

function useRozeniteDomainAgentTool<TInput = unknown, TOutput = unknown>(
function useRozeniteDomainAgentTool<TTool extends AgentTool = AgentTool>(
domain: string,
options: UseRozeniteAgentToolOptions<TInput, TOutput>,
options: UseRozeniteAgentToolOptions<TTool>,
): void {
const { tool, handler, enabled = true } = options;
const toolName = getQualifiedToolName(domain, tool.name);
Expand Down Expand Up @@ -85,7 +92,9 @@ function useRozeniteDomainAgentTool<TInput = unknown, TOutput = unknown>(
}

try {
const result = await handlerRef.current(payload.arguments as TInput);
const result = await handlerRef.current(
payload.arguments as InferAgentToolArgs<TTool>,
);

const response: ToolResultMessage['payload'] = {
callId: payload.callId,
Expand Down Expand Up @@ -134,12 +143,14 @@ function useRozeniteDomainAgentTool<TInput = unknown, TOutput = unknown>(
* The tool is qualified as `{pluginId}.{tool.name}`. It is registered when the
* component mounts (and `enabled` is true) and unregistered on unmount.
*
* @typeParam TInput - Type of the arguments passed to the tool handler (default: `unknown`).
* @typeParam TOutput - Type of the value returned by the handler (default: `unknown`).
* @param options - Configuration: `tool` (AgentTool), `handler`, optional `enabled`, and `pluginId`.
* When `tool` is a typed contract object, handler input/output types are inferred
* from the tool definition. Plain `AgentTool` objects continue to work and fall
* back to `unknown`.
*
* @param options - Configuration using `pluginId`, `tool`, `handler`, and optional `enabled`.
*/
export function useRozenitePluginAgentTool<TInput = unknown, TOutput = unknown>(
options: UseRozenitePluginAgentToolOptions<TInput, TOutput>,
export function useRozenitePluginAgentTool<TTool extends AgentTool = AgentTool>(
options: UseRozenitePluginAgentToolOptions<TTool>,
): void {
const { pluginId, ...toolOptions } = options;
useRozeniteDomainAgentTool(pluginId, toolOptions);
Expand All @@ -152,12 +163,10 @@ export function useRozenitePluginAgentTool<TInput = unknown, TOutput = unknown>(
* The tool is qualified as `app.{tool.name}`. It is registered when the component
* mounts (and `enabled` is true) and unregistered on unmount.
*
* @typeParam TInput - Type of the arguments passed to the tool handler (default: `unknown`).
* @typeParam TOutput - Type of the value returned by the handler (default: `unknown`).
* @param options - Configuration: `tool` (AgentTool), `handler`, and optional `enabled`.
*/
export function useRozeniteInAppAgentTool<TInput = unknown, TOutput = unknown>(
options: UseRozeniteInAppAgentToolOptions<TInput, TOutput>,
export function useRozeniteInAppAgentTool<TTool extends AgentTool = AgentTool>(
options: UseRozeniteInAppAgentToolOptions<TTool>,
): void {
const { ...toolOptions } = options;
useRozeniteDomainAgentTool(APP_DOMAIN, toolOptions);
Expand Down
Loading