Compare commits

..
Author SHA1 Message Date
paseo-bot[bot]andgszep 91154ba4e8 fix(opencode): advertise agent-scoped bridge tool schemas (#7383)
Co-authored-by: gszep <contact@gszep.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-19 16:58:21 +02:00
11 changed files with 236 additions and 219 deletions
+1 -1
View File
@@ -95,7 +95,7 @@ Use OpenCode v2 execution events to trigger turn completion, with active-state a
V2.0.4 also removed the activation endpoint that gated a cold location, and a cold location registers its config-derived commands, skills, and providers asynchronously. Wait until `plugin.list` returns a populated inventory before reading the catalog or commands; an empty inventory means the location is still warming. Fail when the readiness deadline expires, including when an inventory request stalls.
Paseo installs its OpenCode tool bridge through `OPENCODE_CONFIG_CONTENT`. V1 accepts a plugin file; v2 silently skips configured files and requires a package directory with a server entry point. Both versions use the daemon's private loopback bridge for caller-scoped tools. Bridge context lives only in daemon memory and is removed when the Paseo session closes. The content-addressed plugin artifacts contain no session data or secrets. V2 also needs this plugin when native Paseo tools are disabled: its prompt API has no structured-output format, so the plugin supplies a schema-validated final-answer tool.
Paseo installs its OpenCode tool bridge through `OPENCODE_CONFIG_CONTENT`. V1 accepts a plugin file; v2 silently skips configured files and requires a package directory with a server entry point. Both versions use the daemon's private loopback bridge for caller-scoped tools. The global manifest must use agent-scoped schemas before a session is bound, because every bridge call executes in an agent session. Bridge context lives only in daemon memory and is removed when the Paseo session closes. The content-addressed plugin artifacts contain no session data or secrets. V2 also needs this plugin when native Paseo tools are disabled: its prompt API has no structured-output format, so the plugin supplies a schema-validated final-answer tool.
An agent with custom environment variables or user-configured MCP servers gets a dedicated OpenCode server. V2 supports session environment, but its MCP configuration remains location-scoped, so separate servers still prevent one agent from changing another agent's MCP setup. Configure custom MCP with `mcp.add`; do not follow it with `mcp.connect`, which only toggles config-backed servers.
@@ -1,45 +0,0 @@
# Native retained status-ring regression
This runs the production native ring and clock inside real retained panels with
Reanimated on Android. A probe consumes the same public rotation hook and counts
UI-runtime value changes. No Reanimated mocks or provider processes are used.
Use an Android development client compatible with the checkout's native dependencies.
In a disposable checkout, temporarily replace `packages/app/index.ts` with:
```tsx
import "./src/polyfills/install-early";
import "./src/styles/unistyles";
import { registerRootComponent } from "expo";
import NativeRingRegression from "./e2e/mobile/status-ring/native";
registerRootComponent(NativeRingRegression);
```
Start Metro in `packages/app`:
```sh
APP_VARIANT=development npx expo start --localhost --port 8096
```
With one Android emulator running, open that bundle:
```sh
agent-device open sh.paseo.debug 'exp+voice-mobile://expo-development-client/?url=http%3A%2F%2F10.0.2.2%3A8096' --platform android --session status-ring --metro-port 8096
```
From the repository root:
```sh
node packages/app/e2e/mobile/status-ring/android.mjs status-ring http://127.0.0.1:8096
```
The test checks visible rotation, hidden-panel inactivity while another ring is
visible, inactivity with both panels hidden, and synchronized resumption. Each
sample reads counters on the UI runtime; elapsed time includes device-command
latency. The baseline fails at the first hidden-panel assertion.
Restore the original `packages/app/index.ts`, close the agent-device session,
and stop Metro afterward. The fixture substitutes running rings for a live agent
and toggles retained panels directly; it does not exercise workspace navigation
or measure whole-app CPU usage. It is excluded from routine unit tests because
it requires a native runtime.
@@ -1,71 +0,0 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { setTimeout as delay } from "node:timers/promises";
const [, , session, metroUrl] = process.argv;
assert(session && metroUrl, "Pass an agent-device session and Metro URL serving native.tsx");
function ad(...args) {
return execFileSync("agent-device", ["--session", session, ...args], {
encoding: "utf8",
timeout: 60_000,
});
}
function evaluate(expression) {
const value = ad("cdp", "runtime", "eval", "--expr", expression, "--json").trim();
return value === "undefined" ? undefined : JSON.parse(value);
}
let target;
const deadline = Date.now() + 30_000;
do {
target = ad("cdp", "target", "list", "--url", metroUrl)
.split("\n")
.find((line) => line.includes("sh.paseo.debug"));
if (!target) await delay(250);
} while (!target && Date.now() < deadline);
assert(target, "Open the Android dev client on this Metro bundle first");
ad("cdp", "target", "select", target.split(/\s+/)[0]);
let ready = false;
do {
ready = evaluate("Object.keys(globalThis.__ringReaders ?? {}).length === 2");
if (!ready) await delay(250);
} while (!ready && Date.now() < deadline);
assert(ready, "The two native probes must mount before sampling");
async function sample() {
evaluate("Object.values(globalThis.__ringReaders).forEach(read => read())");
await delay(100);
return JSON.parse(evaluate("JSON.stringify(globalThis.__ringSamples)"));
}
async function observe(label) {
const before = await sample();
await delay(600);
const after = await sample();
const deltas = Object.fromEntries(
Object.keys(before).map((key) => [key, after[key].updates - before[key].updates]),
);
console.log(JSON.stringify({ label, deltas, after }));
return { deltas, after };
}
const visible = await observe("both visible");
assert(visible.deltas.first > 0 && visible.deltas.second > 0);
assert.equal(visible.after.first.rotation, visible.after.second.rotation);
ad("press", 'label="HIDE FIRST"');
await delay(200);
const hidden = await observe("first hidden, second visible");
assert.equal(hidden.deltas.first, 0, "A retained hidden ring must stop UI-thread rotation updates");
assert(hidden.deltas.second > 0, "Visible rings must continue rotating");
ad("press", 'label="HIDE SECOND"');
await delay(200);
const allHidden = await observe("both hidden");
assert.equal(allHidden.deltas.first, 0);
assert.equal(allHidden.deltas.second, 0);
ad("press", 'label="SHOW FIRST"');
ad("press", 'label="SHOW SECOND"');
await delay(200);
const restored = await observe("both shown again");
assert(restored.deltas.first > 0 && restored.deltas.second > 0);
assert.equal(
restored.after.first.rotation,
restored.after.second.rotation,
"Restored rings must share the current phase",
);
console.log("PASS native retained-ring lifecycle");
@@ -1,69 +0,0 @@
import { useCallback, useLayoutEffect, useState } from "react";
import { Button, Text, View } from "react-native";
import { useAnimatedReaction, useSharedValue } from "react-native-reanimated";
import { scheduleOnRN, scheduleOnUI } from "react-native-worklets";
import { RetainedPanel } from "@/components/retained-panel";
import { useStatusRingRotation } from "@/components/status-ring/clock";
import { StatusRing } from "@/components/status-ring";
interface Sample {
updates: number;
rotation: number;
}
declare global {
var __ringSamples: Record<string, Sample>;
var __ringReaders: Record<string, () => void>;
}
function recordSample(name: string, value: { updates: number; rotation: number }) {
globalThis.__ringSamples[name] = value;
}
function Probe({ name }: { name: string }) {
const rotation = useStatusRingRotation();
const updates = useSharedValue(0);
useAnimatedReaction(
() => rotation.value,
() => {
updates.value += 1;
},
);
// Read the real UI runtime, without scheduling a continuous JS observer.
useLayoutEffect(() => {
globalThis.__ringSamples ??= {};
globalThis.__ringReaders ??= {};
globalThis.__ringReaders[name] = () =>
scheduleOnUI(() => {
"worklet";
const sample = { updates: updates.value, rotation: rotation.value };
scheduleOnRN(recordSample, name, sample);
});
return () => {
delete globalThis.__ringReaders[name];
};
}, [name, rotation, updates]);
return <StatusRing />;
}
const containerStyle = { flex: 1, padding: 60, backgroundColor: "white" };
export default function NativeRingRegression() {
const [first, setFirst] = useState(true);
const [second, setSecond] = useState(true);
const toggleFirst = useCallback(() => setFirst((value) => !value), []);
const toggleSecond = useCallback(() => setSecond((value) => !value), []);
return (
<View style={containerStyle}>
<Text>Native retained status rings</Text>
<Button title={first ? "Hide first" : "Show first"} onPress={toggleFirst} />
<Button title={second ? "Hide second" : "Show second"} onPress={toggleSecond} />
<RetainedPanel active={first}>
<Probe name="first" />
</RetainedPanel>
<RetainedPanel active={second}>
<Probe name="second" />
</RetainedPanel>
</View>
);
}
@@ -1,13 +1,11 @@
import { useLayoutEffect, useState } from "react";
import { useLayoutEffect } from "react";
import { makeMutable, type SharedValue, useSharedValue } from "react-native-reanimated";
import { scheduleOnUI } from "react-native-worklets";
import { useRetainedPanelActive } from "@/components/retained-panel";
import { getStatusRingRotation } from "@/components/status-ring/geometry";
const sharedRotation = makeMutable(getStatusRingRotation(Date.now()));
const activeRingCount = makeMutable(0);
const clockRunning = makeMutable(false);
let nextListenerId = 1;
function advanceSharedRotation(): void {
"worklet";
@@ -20,21 +18,13 @@ function advanceSharedRotation(): void {
requestAnimationFrame(advanceSharedRotation);
}
function registerStatusRing(
rotation: SharedValue<number>,
registered: SharedValue<boolean>,
listenerId: number,
): void {
function registerStatusRing(registered: SharedValue<boolean>): void {
"worklet";
if (registered.value) {
return;
}
registered.value = true;
rotation.value = getStatusRingRotation(Date.now());
sharedRotation.addListener(listenerId, (value) => {
rotation.value = value;
});
activeRingCount.value += 1;
if (!clockRunning.value) {
@@ -44,31 +34,25 @@ function registerStatusRing(
}
}
function unregisterStatusRing(registered: SharedValue<boolean>, listenerId: number): void {
function unregisterStatusRing(registered: SharedValue<boolean>): void {
"worklet";
if (!registered.value) {
return;
}
registered.value = false;
sharedRotation.removeListener(listenerId);
activeRingCount.value -= 1;
}
export function useStatusRingRotation(): SharedValue<number> {
const active = useRetainedPanelActive();
// Hidden retained views must not subscribe their animated styles to the shared clock.
const rotation = useSharedValue(getStatusRingRotation(Date.now()));
const registered = useSharedValue(false);
const [listenerId] = useState(() => nextListenerId++);
useLayoutEffect(() => {
if (!active) return;
scheduleOnUI(registerStatusRing, rotation, registered, listenerId);
scheduleOnUI(registerStatusRing, registered);
return () => {
scheduleOnUI(unregisterStatusRing, registered, listenerId);
scheduleOnUI(unregisterStatusRing, registered);
};
}, [active, listenerId, registered, rotation]);
}, [registered]);
return rotation;
return sharedRotation;
}
@@ -9,7 +9,7 @@ import type { PaseoToolCatalog } from "./tools/types.js";
export interface AgentProviderRuntime {
snapshotManager: ProviderSnapshotManager;
setPaseoToolCatalog(catalog: PaseoToolCatalog | null): void;
setPaseoToolCatalog(catalog: Pick<PaseoToolCatalog, "tools"> | null): void;
shutdown(): Promise<void>;
}
@@ -13,6 +13,11 @@ import {
getRealProviderRuntimeSettings,
} from "../../daemon-e2e/real-provider-test-config.js";
import type { PaseoToolCatalog } from "../tools/types.js";
import { AgentManager } from "../agent-manager.js";
import { AgentStorage } from "../agent-storage.js";
import { ProviderSnapshotManager } from "../provider-snapshot-manager.js";
import { createPaseoAgentToolManifest, createPaseoToolCatalog } from "../tools/paseo-tools.js";
import { serializePaseoToolInputParameters } from "../tools/paseo-tool-serialization.js";
import { OpenCodeAgentClient } from "./opencode-agent.js";
import { OpenCodeV2AgentClient } from "./opencode/v2/agent.js";
import { OpenCodeBridge } from "./opencode/bridge.js";
@@ -23,6 +28,61 @@ import {
requireSessionId,
} from "./opencode/test-utils/v2-local-e2e-helpers.js";
test("real OpenCode server advertises agent-scoped notification descriptions", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "paseo-opencode-manifest-real-"));
const logger = createTestLogger();
const providerSnapshotManager = new ProviderSnapshotManager({ logger });
const dependencies = {
agentManager: new AgentManager({ logger }),
agentStorage: new AgentStorage(path.join(root, "agents"), logger),
providerSnapshotManager,
logger,
};
const bridge = new OpenCodeBridge({ paseoHome: root, logger });
bridge.setManifestCatalog(createPaseoAgentToolManifest(dependencies));
const manager = new OpenCodeServerManager({
logger,
resolveHomeDir: () => root,
runtimeSettings: {
env: {
XDG_CONFIG_HOME: path.join(root, "config"),
XDG_DATA_HOME: path.join(root, "data"),
XDG_CACHE_HOME: path.join(root, "cache"),
OPENCODE_DISABLE_AUTO_UPDATE: "1",
},
},
decorateServerEnv: (env) => bridge.decorateServerEnv(env),
});
let inspection: Awaited<ReturnType<OpenCodeServerManager["acquireCurrent"]>> | undefined;
try {
await bridge.start();
inspection = await manager.acquireCurrent();
const sdk = createOpencodeClient({ baseUrl: inspection.server.url, directory: root });
const response = await sdk.tool.list({ directory: root, provider: "openai", model: "gpt-5.4" });
expect(response.error).toBeUndefined();
const agentCatalog = createPaseoToolCatalog({ ...dependencies, callerAgentId: "caller" });
for (const name of ["create_agent", "send_agent_prompt"]) {
const actual = response.data!.find((tool) => tool.id === `paseo_${name}`)!;
expect(actual).toBeDefined();
const expected = serializePaseoToolInputParameters(agentCatalog.getTool(name)!);
expect(actual.parameters).toMatchObject({
properties: {
notifyOnFinish: {
description: (expected.properties as Record<string, { description: string }>)
.notifyOnFinish.description,
},
},
});
}
} finally {
await inspection?.release();
await manager.shutdown();
await bridge.close();
await providerSnapshotManager.shutdown();
await rm(root, { recursive: true, force: true });
}
}, 120_000);
test("real OpenCode server persists provider permissions across creation and resume", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "paseo-opencode-permissions-"));
const cwd = path.join(root, "repo");
@@ -5,15 +5,18 @@ import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { z } from "zod";
import Ajv from "ajv";
import { afterEach, describe, expect, test, vi } from "vitest";
import { createTestLogger } from "../../../../test-utils/test-logger.js";
import type { PaseoToolCatalog } from "../../tools/types.js";
import { PaseoToolInputError } from "../../tools/types.js";
import { createPaseoToolCatalog } from "../../tools/paseo-tools.js";
import { AgentManager } from "../../agent-manager.js";
import { AgentStorage } from "../../agent-storage.js";
import { ProviderSnapshotManager } from "../../provider-snapshot-manager.js";
import { createTestAgentClients } from "../../../test-utils/fake-agent-client.js";
import { createPaseoAgentToolManifest, createPaseoToolCatalog } from "../../tools/paseo-tools.js";
import { serializePaseoToolInputParameters } from "../../tools/paseo-tool-serialization.js";
import {
OpenCodeBridge,
loadOpenCodeBridgePluginArtifact,
@@ -72,6 +75,144 @@ function readPluginOptions(env: Record<string, string>): {
}
describe("OpenCodeBridge", () => {
test("advertises agent-scoped schemas that execute through both bridge plugins", async () => {
const paseoHome = await mkdtemp(path.join(tmpdir(), "paseo-opencode-manifest-"));
temporaryDirectories.push(paseoHome);
const logger = createTestLogger();
const clients = createTestAgentClients();
const agentStorage = new AgentStorage(path.join(paseoHome, "agents"), logger);
const agentManager = new AgentManager({ clients, registry: agentStorage, logger });
const providerSnapshotManager = new ProviderSnapshotManager({ logger, extraClients: clients });
const dependencies = { agentManager, agentStorage, providerSnapshotManager, logger };
const bridge = new OpenCodeBridge({ paseoHome, logger });
await bridge.start();
bridge.setManifestCatalog(createPaseoAgentToolManifest(dependencies));
try {
const parent = await agentManager.createAgent(
{ provider: "codex", cwd: paseoHome },
undefined,
{ workspaceId: "manifest-workspace" },
);
const catalog = createPaseoToolCatalog({ ...dependencies, callerAgentId: parent.id });
bridge.bindSession({ sessionId: "bound", env: {}, tools: catalog });
const plugin = readPluginOptions(bridge.decorateServerEnv({}));
const response = await fetch(`${plugin.baseUrl}/_internal/opencode/tools`, {
headers: { Authorization: `Bearer ${plugin.token}` },
});
expect(response.status).toBe(200);
const manifest = z
.object({
tools: z.array(
z.object({
name: z.string(),
description: z.string(),
inputSchema: z.record(z.string(), z.unknown()),
}),
),
})
.parse(await response.json());
const definitions = new Map(manifest.tools.map((tool) => [tool.name, tool]));
const createDefinition = definitions.get("create_agent")!;
const sendDefinition = definitions.get("send_agent_prompt")!;
const topLevelCatalog = createPaseoToolCatalog(dependencies);
for (const name of ["create_agent", "send_agent_prompt"]) {
expect(serializePaseoToolInputParameters(topLevelCatalog.getTool(name)!)).toMatchObject({
properties: { notifyOnFinish: { default: false } },
});
}
const ajv = new Ajv({ useDefaults: true, strict: false });
const createInput = {
title: "Bridge child",
provider: "codex/gpt-5.4",
initialPrompt: "Hello",
notifyOnFinish: false,
};
expect(ajv.compile(createDefinition.inputSchema)(createInput)).toBe(true);
const v2Config = z
.object({ plugins: z.array(z.object({ package: z.string() })) })
.parse(JSON.parse((await bridge.decorateV2ServerEnv({})).OPENCODE_CONFIG_CONTENT));
const v2Module: {
default: { setup(context: V2TestPluginContext): Promise<() => Promise<void>> };
} = await import(
pathToFileURL(path.join(fileURLToPath(v2Config.plugins[0]!.package), "server.js")).href
);
const tools = new Map<string, V2TestTool>();
const dispose = await v2Module.default.setup({
options: plugin,
tool: {
transform: async (transform) => {
transform({
add: (tool) => {
tools.set(tool.name, tool);
},
});
return { dispose: async () => undefined };
},
},
session: {
context: async () => [],
get: async () => ({ parentID: "bound" }),
hook: async () => ({ dispose: async () => undefined }),
},
});
try {
const created = await tools
.get("paseo_create_agent")!
.execute(createInput, { sessionID: "bound" });
const children = agentManager.listAgents().filter((agent) => agent.id !== parent.id);
expect(children).toHaveLength(1);
const child = children[0]!;
expect(created).toMatchObject({
content: [{ type: "text", text: expect.stringContaining(child.id) }],
});
expect(child.workspaceId).toBe("manifest-workspace");
expect([...definitions.keys()]).toEqual([...catalog.tools.keys()]);
for (const definition of manifest.tools) {
const accepted = catalog.getTool(definition.name)!;
expect(definition.inputSchema).toEqual(serializePaseoToolInputParameters(accepted));
expect(definition.description).toBe(accepted.description);
expect(tools.get(`paseo_${definition.name}`)!.input).toEqual(definition.inputSchema);
}
expect(createDefinition.inputSchema).toMatchObject({
properties: { notifyOnFinish: { default: true } },
});
expect(createDefinition.inputSchema.properties).not.toHaveProperty("background");
expect(sendDefinition.inputSchema).toMatchObject({
properties: { notifyOnFinish: { default: true } },
});
await expect(
catalog.executeTool("create_agent", { ...createInput, background: true }),
).rejects.toThrow("background");
const v1Module = await import(plugin.pluginUrl);
const hooks = await v1Module.default({}, plugin);
expect(Object.keys(hooks.tool.paseo_create_agent.args)).toEqual(
Object.keys(
z.record(z.string(), z.unknown()).parse(createDefinition.inputSchema.properties),
),
);
const sendInput = { agentId: child.id, prompt: "Hello again", notifyOnFinish: false };
expect(ajv.compile(sendDefinition.inputSchema)(sendInput)).toBe(true);
await expect(
hooks.tool.paseo_send_agent_prompt.execute(sendInput, {
sessionID: "bound",
ask: async () => undefined,
}),
).resolves.toMatchObject({ output: expect.stringContaining('"success": true') });
} finally {
await dispose();
}
} finally {
await bridge.close();
await Promise.all(
agentManager.listAgents().map((agent) => agentManager.closeAgent(agent.id)),
);
await agentManager.flushForShutdown();
await providerSnapshotManager.shutdown();
}
});
test("loads packaged bundle bytes without invoking source compilation", async () => {
const root = await mkdtemp(path.join(tmpdir(), "paseo-opencode-artifact-"));
temporaryDirectories.push(root);
@@ -696,6 +837,7 @@ describe("OpenCodeBridge", () => {
interface V2TestTool {
name: string;
input?: unknown;
execute(input: unknown, call: { sessionID: string }): Promise<unknown>;
}
interface V2TestContext {
@@ -60,7 +60,7 @@ export class OpenCodeBridge {
private baseUrl: string | null = null;
private pluginUrl: string | null = null;
private v2PluginUrl: string | null = null;
private manifestCatalog: PaseoToolCatalog | null = null;
private manifestCatalog: Pick<PaseoToolCatalog, "tools"> | null = null;
constructor(options: OpenCodeBridgeOptions) {
this.paseoHome = options.paseoHome;
@@ -89,7 +89,7 @@ export class OpenCodeBridge {
this.baseUrl = `http://127.0.0.1:${address.port}`;
}
setManifestCatalog(catalog: PaseoToolCatalog | null): void {
setManifestCatalog(catalog: Pick<PaseoToolCatalog, "tools"> | null): void {
this.manifestCatalog = catalog;
}
@@ -637,6 +637,20 @@ function resolveTerminalKeyToken({ key, literal, preamble }: TerminalKeyTokenInp
}
export function createPaseoToolCatalog(options: PaseoToolHostDependencies): PaseoToolCatalog {
return createToolCatalog(options, options.callerAgentId ? "agent" : "top-level");
}
export function createPaseoAgentToolManifest(
options: PaseoToolHostDependencies,
): Pick<PaseoToolCatalog, "tools"> {
// Plugins register before a caller exists, but every execution uses an agent-bound catalog.
return { tools: createToolCatalog(options, "agent").tools };
}
function createToolCatalog(
options: PaseoToolHostDependencies,
scope: "agent" | "top-level",
): PaseoToolCatalog {
const {
agentManager,
agentStorage,
@@ -1209,7 +1223,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase
.describe("Legacy GitHub PR number. Prefer workspace.source.target.githubPrNumber."),
};
const createAgentInputSchema = z
.object(callerAgentId ? agentToAgentInputSchema : canonicalTopLevelInputSchema)
.object(scope === "agent" ? agentToAgentInputSchema : canonicalTopLevelInputSchema)
.passthrough();
const agentToAgentCreateAgentArgsSchema = z.object(agentToAgentInputSchema).strict();
const legacyAgentToAgentCreateAgentArgsSchema = z.object(legacyAgentToAgentInputSchema).strict();
@@ -1245,9 +1259,8 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase
"Agent-scoped only: get notified when the prompted agent finishes, errors, or needs permission.",
),
};
const sendAgentPromptInputSchema = callerAgentId
? agentToAgentSendAgentPromptInputSchema
: topLevelSendAgentPromptInputSchema;
const sendAgentPromptInputSchema =
scope === "agent" ? agentToAgentSendAgentPromptInputSchema : topLevelSendAgentPromptInputSchema;
const inspectProviderInputSchema = {
provider: ProviderOrProviderModelInputSchema.describe(
"Provider ID, optionally with a model ID (for example codex or codex/gpt-5.4).",
@@ -2653,7 +2666,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase
.optional()
.describe("IANA time zone for the cron cadence. For example: America/New_York."),
name: z.string().optional(),
provider: (callerAgentId ? AgentProviderEnum.optional() : AgentProviderEnum).describe(
provider: (scope === "agent" ? AgentProviderEnum.optional() : AgentProviderEnum).describe(
"Provider, or provider/model (for example: codex or codex/gpt-5.4). Defaults to the caller's provider in an agent-scoped session.",
),
cwd: z.string().optional(),
+4 -1
View File
@@ -137,6 +137,7 @@ import { attachAgentStoragePersistence } from "./persistence-hooks.js";
import { createAgentMcpServer } from "./agent/mcp-server.js";
import {
createPaseoToolCatalog,
createPaseoAgentToolManifest,
type PaseoToolHostDependencies,
} from "./agent/tools/paseo-tools.js";
import type { PaseoToolRuntimeContext } from "./agent/tools/types.js";
@@ -1457,7 +1458,9 @@ export async function createPaseoDaemon(
const createAgentToolCatalog = (runtime: PaseoToolRuntimeContext) =>
createPaseoToolCatalog(createAgentToolHostDependencies(runtime));
const setAgentProviderToolsEnabled = (enabled: boolean) => {
agentProviderRuntime.setPaseoToolCatalog(enabled ? createAgentToolCatalog({}) : null);
agentProviderRuntime.setPaseoToolCatalog(
enabled ? createPaseoAgentToolManifest(createAgentToolHostDependencies({})) : null,
);
};
agentManager.setPaseoToolCatalogFactory(createAgentToolCatalog);
agentManager.setPaseoToolsEnabled(config.mcpInjectIntoAgents !== false);