mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-19 15:52:49 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a73d93ec36 |
+1
-1
@@ -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. 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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package sh.paseo.diagnosis;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.text.Layout;
|
||||
import android.text.Spannable;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import com.facebook.react.bridge.BridgeReactContext;
|
||||
import com.facebook.react.common.mapbuffer.WritableMapBuffer;
|
||||
import com.facebook.react.soloader.OpenSourceMergedSoMapping;
|
||||
import com.facebook.react.uimanager.DisplayMetricsHolder;
|
||||
import com.facebook.react.uimanager.PixelUtil;
|
||||
import com.facebook.react.uimanager.ThemedReactContext;
|
||||
import com.facebook.react.views.text.ReactTextUpdate;
|
||||
import com.facebook.react.views.text.ReactTextView;
|
||||
import com.facebook.react.views.text.TextLayoutManager;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
import com.facebook.yoga.YogaMeasureMode;
|
||||
import com.facebook.yoga.YogaMeasureOutput;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/** Native reproduction for #4368. Uses the measurement and rendering paths used by RN Text. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class AboutFontWeightTest {
|
||||
@Test public void ordinaryVersionFitsItsMeasuredBox() { checkVersion(0); }
|
||||
@Test public void boldVersionFitsItsMeasuredBox() { checkVersion(300); }
|
||||
|
||||
private void checkVersion(int adjustment) {
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
|
||||
Context app = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
Configuration configuration = new Configuration(app.getResources().getConfiguration());
|
||||
configuration.fontWeightAdjustment = adjustment;
|
||||
Context configured = app.createConfigurationContext(configuration);
|
||||
try {
|
||||
SoLoader.init(app, OpenSourceMergedSoMapping.INSTANCE);
|
||||
} catch (java.io.IOException error) {
|
||||
throw new AssertionError("Could not initialize React Native", error);
|
||||
}
|
||||
DisplayMetricsHolder.initDisplayMetrics(configured);
|
||||
ThemedReactContext context = new ThemedReactContext(new BridgeReactContext(configured), configured);
|
||||
|
||||
WritableMapBuffer attributes = new WritableMapBuffer();
|
||||
attributes.put(4, 15.0); // TextAttributeProps.TA_KEY_FONT_SIZE
|
||||
attributes.put(9, true); // TextAttributeProps.TA_KEY_ALLOW_FONT_SCALING
|
||||
WritableMapBuffer fragment = new WritableMapBuffer();
|
||||
fragment.put(0, "v0.12.0");
|
||||
fragment.put(1, 1);
|
||||
fragment.put(5, attributes);
|
||||
WritableMapBuffer fragments = new WritableMapBuffer();
|
||||
fragments.put(0, fragment);
|
||||
WritableMapBuffer text = new WritableMapBuffer();
|
||||
text.put(1, "v0.12.0");
|
||||
text.put(2, fragments);
|
||||
text.put(4, attributes);
|
||||
WritableMapBuffer paragraph = new WritableMapBuffer();
|
||||
paragraph.put(2, "highQuality");
|
||||
paragraph.put(5, "none");
|
||||
|
||||
long measured = TextLayoutManager.measureText(context, text, paragraph,
|
||||
1000, YogaMeasureMode.AT_MOST, 1000, YogaMeasureMode.AT_MOST, null, null);
|
||||
int width = (int) Math.ceil(PixelUtil.toPixelFromDIP(YogaMeasureOutput.getWidth(measured)));
|
||||
int height = (int) Math.ceil(PixelUtil.toPixelFromDIP(YogaMeasureOutput.getHeight(measured)));
|
||||
Spannable content = TextLayoutManager.INSTANCE.getOrCreateSpannableForText(context, text, null);
|
||||
ReactTextView view = new ReactTextView(context);
|
||||
view.setId(2); // Fabric tag, matching the app's new-architecture Text path.
|
||||
view.setText(new ReactTextUpdate(content, -1, false, 0,
|
||||
Layout.BREAK_STRATEGY_HIGH_QUALITY, Layout.JUSTIFICATION_MODE_NONE));
|
||||
view.measure(exact(width), exact(height));
|
||||
view.layout(0, 0, width, height);
|
||||
Layout rendered = view.getLayout();
|
||||
Log.i("AboutFontWeightTest", "adjustment=" + adjustment + " box=" + width + "x" + height
|
||||
+ " lines=" + rendered.getLineCount() + " lineWidth=" + rendered.getLineWidth(0)
|
||||
+ " weight=" + view.getTypeface().getWeight());
|
||||
assertEquals("The complete version should fit on its measured line", 1, rendered.getLineCount());
|
||||
assertTrue("The last digit must remain inside the allocated width",
|
||||
rendered.getLineWidth(0) <= width);
|
||||
assertTrue("The complete line must remain inside the allocated height",
|
||||
rendered.getLineBottom(0) <= height);
|
||||
});
|
||||
}
|
||||
|
||||
private static int exact(int size) {
|
||||
return View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.EXACTLY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
# About version clipping reproduction ([#4368](https://github.com/getpaseo/paseo/issues/4368))
|
||||
|
||||
This is a diagnostic branch, with no production fix. The Android test compares
|
||||
React Native's measured box for `v0.12.0` with the native TextView layout that
|
||||
renders the same attributed text. It runs both ordinary and system-bold text.
|
||||
It does not turn off accessibility scaling or bold text.
|
||||
|
||||
## Run the native test
|
||||
|
||||
From the repository root, prepare a development Android project if necessary:
|
||||
|
||||
```sh
|
||||
cd packages/app
|
||||
APP_VARIANT=development npx expo prebuild --platform android --no-install
|
||||
```
|
||||
|
||||
In generated `android/app/build.gradle`, add:
|
||||
|
||||
```groovy
|
||||
android.defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
dependencies {
|
||||
androidTestImplementation 'androidx.test:runner:1.6.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||
}
|
||||
```
|
||||
|
||||
Copy this test into the generated test source set, then build the app and tests:
|
||||
|
||||
```sh
|
||||
mkdir -p android/app/src/androidTest/java/sh/paseo/diagnosis
|
||||
cp e2e/mobile/about-font-size/AboutFontWeightTest.java android/app/src/androidTest/java/sh/paseo/diagnosis/
|
||||
cd android
|
||||
APP_VARIANT=development ./gradlew :app:assembleDebug :app:assembleDebugAndroidTest \
|
||||
-PreactNativeArchitectures=x86_64 --no-daemon --max-workers=2 -Dorg.gradle.parallel=false
|
||||
adb install -r app/build/outputs/apk/debug/app-debug.apk
|
||||
adb install -r app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk
|
||||
adb shell am instrument -w -e class sh.paseo.diagnosis.AboutFontWeightTest \
|
||||
sh.paseo.debug.test/androidx.test.runner.AndroidJUnitRunner
|
||||
```
|
||||
|
||||
The test requires Android 12 or later for `fontWeightAdjustment`. The native
|
||||
contexts set that adjustment to 0 and 300 independently; they do not change the
|
||||
device setting. The selected device's density and font scale still apply.
|
||||
|
||||
On Android 16 at 540 dpi and 2× font scale, both contexts receive a 301×123-pixel
|
||||
box. The ordinary-weight control passes, with one rendered line. The bold test
|
||||
fails with two rendered lines, so the final digit falls outside the allocated
|
||||
single-line height. This isolates the measurement/rendering disagreement from
|
||||
the About row's flex layout. A native dependency/build decision is still needed
|
||||
before implementing a fix; changing an About-row style would leave the producer
|
||||
of the incorrect measurement in place.
|
||||
|
||||
## UI comparison
|
||||
|
||||
Observed on Android 16, with current main's JavaScript and a freshly built native
|
||||
development app: open Settings → About with 2× system font scale and 540 dpi on
|
||||
a 1080-pixel-wide emulator. With system bold text, the displayed app version
|
||||
loses its final digit (`v0.12.`). At the same display and text sizes without
|
||||
system bold, the complete `v0.12.0` is visible. Cold-launch after configuration
|
||||
changes before comparing; changing font scale in a running app also produced
|
||||
temporary vertical clipping, which disappeared after a cold launch.
|
||||
|
||||
This substitutes standard Android accessibility settings for Samsung Easy Mode.
|
||||
It verifies the truncated-version symptom, not Samsung's complete UI or the
|
||||
already-fixed home-screen overlap. The reporter used Paseo 0.7.2; this branch
|
||||
uses main `dd244c076d5f17d56d5592c7fb173dc7fdc2bbae`.
|
||||
@@ -9,7 +9,7 @@ import type { PaseoToolCatalog } from "./tools/types.js";
|
||||
|
||||
export interface AgentProviderRuntime {
|
||||
snapshotManager: ProviderSnapshotManager;
|
||||
setPaseoToolCatalog(catalog: Pick<PaseoToolCatalog, "tools"> | null): void;
|
||||
setPaseoToolCatalog(catalog: PaseoToolCatalog | null): void;
|
||||
shutdown(): Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,6 @@ 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";
|
||||
@@ -28,61 +23,6 @@ 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,18 +5,15 @@ 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,
|
||||
@@ -75,144 +72,6 @@ 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);
|
||||
@@ -837,7 +696,6 @@ 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: Pick<PaseoToolCatalog, "tools"> | null = null;
|
||||
private manifestCatalog: PaseoToolCatalog | 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: Pick<PaseoToolCatalog, "tools"> | null): void {
|
||||
setManifestCatalog(catalog: PaseoToolCatalog | null): void {
|
||||
this.manifestCatalog = catalog;
|
||||
}
|
||||
|
||||
|
||||
@@ -637,20 +637,6 @@ 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,
|
||||
@@ -1223,7 +1209,7 @@ function createToolCatalog(
|
||||
.describe("Legacy GitHub PR number. Prefer workspace.source.target.githubPrNumber."),
|
||||
};
|
||||
const createAgentInputSchema = z
|
||||
.object(scope === "agent" ? agentToAgentInputSchema : canonicalTopLevelInputSchema)
|
||||
.object(callerAgentId ? agentToAgentInputSchema : canonicalTopLevelInputSchema)
|
||||
.passthrough();
|
||||
const agentToAgentCreateAgentArgsSchema = z.object(agentToAgentInputSchema).strict();
|
||||
const legacyAgentToAgentCreateAgentArgsSchema = z.object(legacyAgentToAgentInputSchema).strict();
|
||||
@@ -1259,8 +1245,9 @@ function createToolCatalog(
|
||||
"Agent-scoped only: get notified when the prompted agent finishes, errors, or needs permission.",
|
||||
),
|
||||
};
|
||||
const sendAgentPromptInputSchema =
|
||||
scope === "agent" ? agentToAgentSendAgentPromptInputSchema : topLevelSendAgentPromptInputSchema;
|
||||
const sendAgentPromptInputSchema = callerAgentId
|
||||
? agentToAgentSendAgentPromptInputSchema
|
||||
: topLevelSendAgentPromptInputSchema;
|
||||
const inspectProviderInputSchema = {
|
||||
provider: ProviderOrProviderModelInputSchema.describe(
|
||||
"Provider ID, optionally with a model ID (for example codex or codex/gpt-5.4).",
|
||||
@@ -2666,7 +2653,7 @@ function createToolCatalog(
|
||||
.optional()
|
||||
.describe("IANA time zone for the cron cadence. For example: America/New_York."),
|
||||
name: z.string().optional(),
|
||||
provider: (scope === "agent" ? AgentProviderEnum.optional() : AgentProviderEnum).describe(
|
||||
provider: (callerAgentId ? 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(),
|
||||
|
||||
@@ -137,7 +137,6 @@ 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";
|
||||
@@ -1458,9 +1457,7 @@ export async function createPaseoDaemon(
|
||||
const createAgentToolCatalog = (runtime: PaseoToolRuntimeContext) =>
|
||||
createPaseoToolCatalog(createAgentToolHostDependencies(runtime));
|
||||
const setAgentProviderToolsEnabled = (enabled: boolean) => {
|
||||
agentProviderRuntime.setPaseoToolCatalog(
|
||||
enabled ? createPaseoAgentToolManifest(createAgentToolHostDependencies({})) : null,
|
||||
);
|
||||
agentProviderRuntime.setPaseoToolCatalog(enabled ? createAgentToolCatalog({}) : null);
|
||||
};
|
||||
agentManager.setPaseoToolCatalogFactory(createAgentToolCatalog);
|
||||
agentManager.setPaseoToolsEnabled(config.mcpInjectIntoAgents !== false);
|
||||
|
||||
@@ -288,12 +288,6 @@ async function runScenario(modeId: "auto" | "auto-review") {
|
||||
try {
|
||||
writeMockCodexConfig(codexHome, mockServer.url);
|
||||
await client.connect();
|
||||
const permissionEvents = client.observeEvents([
|
||||
"agent_permission_request",
|
||||
"agent_permission_resolved",
|
||||
]);
|
||||
permissionEvents.subscribe({ snapshot: () => {}, update: () => {} });
|
||||
await permissionEvents.ready;
|
||||
await client.fetchAgents({ subscribe: {} });
|
||||
|
||||
const agent = await client.createAgent({
|
||||
|
||||
Reference in New Issue
Block a user