mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Compare commits
1 Commits
v0.1.103
...
feat-paseo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3448e8b023 |
@@ -28,6 +28,7 @@ import type {
|
|||||||
PersistedAgentDescriptor,
|
PersistedAgentDescriptor,
|
||||||
} from "./agent-sdk-types.js";
|
} from "./agent-sdk-types.js";
|
||||||
import type { ProviderDefinition } from "./provider-registry.js";
|
import type { ProviderDefinition } from "./provider-registry.js";
|
||||||
|
import { PaseoToolingRuntime } from "./paseo-tooling/runtime.js";
|
||||||
|
|
||||||
interface Deferred<T> {
|
interface Deferred<T> {
|
||||||
promise: Promise<T>;
|
promise: Promise<T>;
|
||||||
@@ -522,7 +523,20 @@ test("createAgent injects daemon append system prompt at runtime only", async ()
|
|||||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
||||||
const storagePath = join(workdir, "agents");
|
const storagePath = join(workdir, "agents");
|
||||||
const storage = new AgentStorage(storagePath, logger);
|
const storage = new AgentStorage(storagePath, logger);
|
||||||
const client = new TestAgentClient();
|
class CaptureClient extends TestAgentClient {
|
||||||
|
lastLaunchContext: AgentLaunchContext | null = null;
|
||||||
|
|
||||||
|
override async resumeSession(
|
||||||
|
handle: AgentPersistenceHandle,
|
||||||
|
config?: Partial<AgentSessionConfig>,
|
||||||
|
launchContext?: AgentLaunchContext,
|
||||||
|
): Promise<AgentSession> {
|
||||||
|
this.lastLaunchContext = launchContext ?? null;
|
||||||
|
return super.resumeSession(handle, config, launchContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new CaptureClient();
|
||||||
const manager = new AgentManager({
|
const manager = new AgentManager({
|
||||||
clients: {
|
clients: {
|
||||||
codex: client,
|
codex: client,
|
||||||
@@ -551,7 +565,20 @@ test("daemon append system prompt is injected into Pi configs", async () => {
|
|||||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
||||||
const storagePath = join(workdir, "agents");
|
const storagePath = join(workdir, "agents");
|
||||||
const storage = new AgentStorage(storagePath, logger);
|
const storage = new AgentStorage(storagePath, logger);
|
||||||
const client = new TestAgentClient();
|
class CaptureClient extends TestAgentClient {
|
||||||
|
lastLaunchContext: AgentLaunchContext | null = null;
|
||||||
|
|
||||||
|
override async resumeSession(
|
||||||
|
handle: AgentPersistenceHandle,
|
||||||
|
config?: Partial<AgentSessionConfig>,
|
||||||
|
launchContext?: AgentLaunchContext,
|
||||||
|
): Promise<AgentSession> {
|
||||||
|
this.lastLaunchContext = launchContext ?? null;
|
||||||
|
return super.resumeSession(handle, config, launchContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new CaptureClient();
|
||||||
const manager = new AgentManager({
|
const manager = new AgentManager({
|
||||||
clients: {
|
clients: {
|
||||||
pi: client as unknown as AgentClient,
|
pi: client as unknown as AgentClient,
|
||||||
@@ -948,28 +975,38 @@ test("createAgent passes persistSession to provider create options", async () =>
|
|||||||
rmSync(workdir, { recursive: true, force: true });
|
rmSync(workdir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("createAgent injects paseo MCP server only into provider launch config", async () => {
|
test("createAgent passes internal paseo tooling only through runtime launch context", async () => {
|
||||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
||||||
const storagePath = join(workdir, "agents");
|
const storagePath = join(workdir, "agents");
|
||||||
const storage = new AgentStorage(storagePath, logger);
|
const storage = new AgentStorage(storagePath, logger);
|
||||||
|
|
||||||
class CaptureClient extends TestAgentClient {
|
class CaptureClient extends TestAgentClient {
|
||||||
lastConfig: AgentSessionConfig | null = null;
|
lastConfig: AgentSessionConfig | null = null;
|
||||||
|
lastLaunchContext: AgentLaunchContext | null = null;
|
||||||
|
|
||||||
override async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
override async createSession(
|
||||||
|
config: AgentSessionConfig,
|
||||||
|
launchContext?: AgentLaunchContext,
|
||||||
|
): Promise<AgentSession> {
|
||||||
this.lastConfig = config;
|
this.lastConfig = config;
|
||||||
|
this.lastLaunchContext = launchContext ?? null;
|
||||||
return new TestAgentSession(config);
|
return new TestAgentSession(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = new CaptureClient();
|
const client = new CaptureClient();
|
||||||
|
const paseoToolingRuntime = new PaseoToolingRuntime();
|
||||||
|
paseoToolingRuntime.setEndpoints({
|
||||||
|
mcpBaseUrl: "http://127.0.0.1:6767/mcp/agents",
|
||||||
|
httpBaseUrl: "http://127.0.0.1:6767",
|
||||||
|
});
|
||||||
const manager = new AgentManager({
|
const manager = new AgentManager({
|
||||||
clients: {
|
clients: {
|
||||||
codex: client,
|
codex: client,
|
||||||
},
|
},
|
||||||
registry: storage,
|
registry: storage,
|
||||||
logger,
|
logger,
|
||||||
mcpBaseUrl: "http://127.0.0.1:6767/mcp/agents",
|
paseoToolingRuntime,
|
||||||
idFactory: () => "00000000-0000-4000-8000-000000000103",
|
idFactory: () => "00000000-0000-4000-8000-000000000103",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -991,15 +1028,15 @@ test("createAgent injects paseo MCP server only into provider launch config", as
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(client.lastConfig?.mcpServers).toEqual({
|
expect(client.lastConfig?.mcpServers).toEqual({
|
||||||
paseo: {
|
|
||||||
type: "http",
|
|
||||||
url: `http://127.0.0.1:6767/mcp/agents?callerAgentId=${snapshot.id}`,
|
|
||||||
},
|
|
||||||
custom: {
|
custom: {
|
||||||
type: "stdio",
|
type: "stdio",
|
||||||
command: "custom-mcp",
|
command: "custom-mcp",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
expect(client.lastLaunchContext?.paseoTooling?.mcpUrl).toBe(
|
||||||
|
`http://127.0.0.1:6767/mcp/agents?callerAgentId=${snapshot.id}`,
|
||||||
|
);
|
||||||
|
expect(client.lastLaunchContext?.paseoTooling?.httpBaseUrl).toBe("http://127.0.0.1:6767");
|
||||||
|
|
||||||
const stored = await storage.get(snapshot.id);
|
const stored = await storage.get(snapshot.id);
|
||||||
expect(stored?.config?.mcpServers).toEqual({
|
expect(stored?.config?.mcpServers).toEqual({
|
||||||
@@ -1010,18 +1047,36 @@ test("createAgent injects paseo MCP server only into provider launch config", as
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("resumeAgentFromPersistence replaces stored internal paseo MCP with current runtime URL", async () => {
|
test("resumeAgentFromPersistence strips stored internal paseo MCP and passes current runtime tooling", async () => {
|
||||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
||||||
const storagePath = join(workdir, "agents");
|
const storagePath = join(workdir, "agents");
|
||||||
const storage = new AgentStorage(storagePath, logger);
|
const storage = new AgentStorage(storagePath, logger);
|
||||||
const client = new TestAgentClient();
|
class CaptureClient extends TestAgentClient {
|
||||||
|
lastLaunchContext: AgentLaunchContext | null = null;
|
||||||
|
|
||||||
|
override async resumeSession(
|
||||||
|
handle: AgentPersistenceHandle,
|
||||||
|
config?: Partial<AgentSessionConfig>,
|
||||||
|
launchContext?: AgentLaunchContext,
|
||||||
|
): Promise<AgentSession> {
|
||||||
|
this.lastLaunchContext = launchContext ?? null;
|
||||||
|
return super.resumeSession(handle, config, launchContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new CaptureClient();
|
||||||
|
const paseoToolingRuntime = new PaseoToolingRuntime();
|
||||||
|
paseoToolingRuntime.setEndpoints({
|
||||||
|
mcpBaseUrl: "http://127.0.0.1:6768/mcp/agents",
|
||||||
|
httpBaseUrl: "http://127.0.0.1:6768",
|
||||||
|
});
|
||||||
const manager = new AgentManager({
|
const manager = new AgentManager({
|
||||||
clients: {
|
clients: {
|
||||||
codex: client,
|
codex: client,
|
||||||
},
|
},
|
||||||
registry: storage,
|
registry: storage,
|
||||||
logger,
|
logger,
|
||||||
mcpBaseUrl: "http://127.0.0.1:6768/mcp/agents",
|
paseoToolingRuntime,
|
||||||
idFactory: () => "00000000-0000-4000-8000-000000000105",
|
idFactory: () => "00000000-0000-4000-8000-000000000105",
|
||||||
});
|
});
|
||||||
const handle: AgentPersistenceHandle = {
|
const handle: AgentPersistenceHandle = {
|
||||||
@@ -1047,15 +1102,14 @@ test("resumeAgentFromPersistence replaces stored internal paseo MCP with current
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(client.resumeOverrides[0]?.mcpServers).toEqual({
|
expect(client.resumeOverrides[0]?.mcpServers).toEqual({
|
||||||
paseo: {
|
|
||||||
type: "http",
|
|
||||||
url: `http://127.0.0.1:6768/mcp/agents?callerAgentId=${snapshot.id}`,
|
|
||||||
},
|
|
||||||
custom: {
|
custom: {
|
||||||
type: "stdio",
|
type: "stdio",
|
||||||
command: "custom-mcp",
|
command: "custom-mcp",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
expect(client.lastLaunchContext?.paseoTooling?.mcpUrl).toBe(
|
||||||
|
`http://127.0.0.1:6768/mcp/agents?callerAgentId=${snapshot.id}`,
|
||||||
|
);
|
||||||
expect(snapshot.config.mcpServers).toEqual({
|
expect(snapshot.config.mcpServers).toEqual({
|
||||||
custom: {
|
custom: {
|
||||||
type: "stdio",
|
type: "stdio",
|
||||||
@@ -1119,7 +1173,6 @@ test("createAgent preserves a user-provided paseo MCP config", async () => {
|
|||||||
},
|
},
|
||||||
registry: storage,
|
registry: storage,
|
||||||
logger,
|
logger,
|
||||||
mcpBaseUrl: "http://127.0.0.1:6767/mcp/agents",
|
|
||||||
idFactory: () => "00000000-0000-4000-8000-000000000104",
|
idFactory: () => "00000000-0000-4000-8000-000000000104",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ import { getAgentProviderDefinition } from "@getpaseo/protocol/provider-manifest
|
|||||||
import { IMPORTABLE_PROVIDERS } from "./provider-registry.js";
|
import { IMPORTABLE_PROVIDERS } from "./provider-registry.js";
|
||||||
import { invokeRewindCapability, type RewindMode } from "./rewind/rewind.js";
|
import { invokeRewindCapability, type RewindMode } from "./rewind/rewind.js";
|
||||||
import { isSystemInjectedEnvelope } from "./agent-prompt.js";
|
import { isSystemInjectedEnvelope } from "./agent-prompt.js";
|
||||||
import { stripInternalPaseoMcpServer, withRuntimePaseoMcpServer } from "./runtime-mcp-config.js";
|
import { stripInternalPaseoMcpServer } from "./runtime-mcp-config.js";
|
||||||
|
import type { PaseoToolingRuntime } from "./paseo-tooling/runtime.js";
|
||||||
|
|
||||||
const RELOAD_SESSION_CLOSE_TIMEOUT_MS = 3_000;
|
const RELOAD_SESSION_CLOSE_TIMEOUT_MS = 3_000;
|
||||||
const INTERRUPT_SESSION_TIMEOUT_MS = 2_000;
|
const INTERRUPT_SESSION_TIMEOUT_MS = 2_000;
|
||||||
@@ -189,7 +190,7 @@ export interface AgentManagerOptions {
|
|||||||
onAgentAttention?: AgentAttentionCallback;
|
onAgentAttention?: AgentAttentionCallback;
|
||||||
durableTimelineStore?: AgentTimelineStore;
|
durableTimelineStore?: AgentTimelineStore;
|
||||||
terminalManager?: TerminalManager | null;
|
terminalManager?: TerminalManager | null;
|
||||||
mcpBaseUrl?: string;
|
paseoToolingRuntime?: PaseoToolingRuntime;
|
||||||
appendSystemPrompt?: string;
|
appendSystemPrompt?: string;
|
||||||
agentStreamCoalesceWindowMs?: number;
|
agentStreamCoalesceWindowMs?: number;
|
||||||
rescueTimeouts?: AgentManagerRescueTimeouts;
|
rescueTimeouts?: AgentManagerRescueTimeouts;
|
||||||
@@ -432,7 +433,7 @@ export class AgentManager {
|
|||||||
private readonly previousStatuses = new Map<string, AgentLifecycleStatus>();
|
private readonly previousStatuses = new Map<string, AgentLifecycleStatus>();
|
||||||
private readonly backgroundTasks = new Set<Promise<void>>();
|
private readonly backgroundTasks = new Set<Promise<void>>();
|
||||||
private readonly agentStreamCoalescer: AgentStreamCoalescer;
|
private readonly agentStreamCoalescer: AgentStreamCoalescer;
|
||||||
private mcpBaseUrl: string | null;
|
private readonly paseoToolingRuntime: PaseoToolingRuntime | null;
|
||||||
private appendSystemPrompt: string;
|
private appendSystemPrompt: string;
|
||||||
private onAgentAttention?: AgentAttentionCallback;
|
private onAgentAttention?: AgentAttentionCallback;
|
||||||
private onAgentArchived?: AgentArchivedCallback;
|
private onAgentArchived?: AgentArchivedCallback;
|
||||||
@@ -444,7 +445,7 @@ export class AgentManager {
|
|||||||
this.registry = options?.registry;
|
this.registry = options?.registry;
|
||||||
this.durableTimelineStore = options?.durableTimelineStore;
|
this.durableTimelineStore = options?.durableTimelineStore;
|
||||||
this.onAgentAttention = options?.onAgentAttention;
|
this.onAgentAttention = options?.onAgentAttention;
|
||||||
this.mcpBaseUrl = options?.mcpBaseUrl ?? null;
|
this.paseoToolingRuntime = options?.paseoToolingRuntime ?? null;
|
||||||
this.appendSystemPrompt = options.appendSystemPrompt ?? "";
|
this.appendSystemPrompt = options.appendSystemPrompt ?? "";
|
||||||
this.logger = options.logger.child({ module: "agent", component: "agent-manager" });
|
this.logger = options.logger.child({ module: "agent", component: "agent-manager" });
|
||||||
this.rescueTimeouts = {
|
this.rescueTimeouts = {
|
||||||
@@ -500,10 +501,6 @@ export class AgentManager {
|
|||||||
this.onAgentArchived = callback;
|
this.onAgentArchived = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMcpBaseUrl(url: string | null): void {
|
|
||||||
this.mcpBaseUrl = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
setAppendSystemPrompt(prompt: string | null | undefined): void {
|
setAppendSystemPrompt(prompt: string | null | undefined): void {
|
||||||
this.appendSystemPrompt = prompt ?? "";
|
this.appendSystemPrompt = prompt ?? "";
|
||||||
}
|
}
|
||||||
@@ -3460,16 +3457,10 @@ export class AgentManager {
|
|||||||
|
|
||||||
private async prepareSessionConfig(
|
private async prepareSessionConfig(
|
||||||
config: AgentSessionConfig,
|
config: AgentSessionConfig,
|
||||||
agentId: string,
|
_agentId: string,
|
||||||
): Promise<PreparedSessionConfig> {
|
): Promise<PreparedSessionConfig> {
|
||||||
const storedConfig = await this.normalizeConfig(stripInternalPaseoMcpServer(config));
|
const storedConfig = await this.normalizeConfig(stripInternalPaseoMcpServer(config));
|
||||||
const launchConfig = this.applyDaemonAppendSystemPrompt(
|
const launchConfig = this.applyDaemonAppendSystemPrompt(storedConfig);
|
||||||
withRuntimePaseoMcpServer({
|
|
||||||
config: storedConfig,
|
|
||||||
agentId,
|
|
||||||
mcpBaseUrl: this.mcpBaseUrl,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return { storedConfig, launchConfig };
|
return { storedConfig, launchConfig };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3489,6 +3480,7 @@ export class AgentManager {
|
|||||||
private buildLaunchContext(agentId: string, env?: Record<string, string>): AgentLaunchContext {
|
private buildLaunchContext(agentId: string, env?: Record<string, string>): AgentLaunchContext {
|
||||||
return {
|
return {
|
||||||
agentId,
|
agentId,
|
||||||
|
paseoTooling: this.paseoToolingRuntime?.createLaunchContext(agentId),
|
||||||
env: {
|
env: {
|
||||||
...env,
|
...env,
|
||||||
PASEO_AGENT_ID: agentId,
|
PASEO_AGENT_ID: agentId,
|
||||||
|
|||||||
@@ -546,9 +546,23 @@ export interface AgentSessionConfig {
|
|||||||
internal?: boolean;
|
internal?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PaseoToolingProviderSessionRef {
|
||||||
|
provider: AgentProvider;
|
||||||
|
sessionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaseoToolingLaunchContext {
|
||||||
|
agentId: string;
|
||||||
|
mcpUrl: string | null;
|
||||||
|
httpBaseUrl: string | null;
|
||||||
|
token: string;
|
||||||
|
bindProviderSession(ref: PaseoToolingProviderSessionRef): () => void;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AgentLaunchContext {
|
export interface AgentLaunchContext {
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
|
paseoTooling?: PaseoToolingLaunchContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentCreateSessionOptions {
|
export interface AgentCreateSessionOptions {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
101
packages/server/src/server/agent/paseo-tooling/http-adapter.ts
Normal file
101
packages/server/src/server/agent/paseo-tooling/http-adapter.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import type express from "express";
|
||||||
|
import type { Logger } from "pino";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import type { AgentProvider } from "../agent-sdk-types.js";
|
||||||
|
import type { PaseoToolRegistry } from "./registry.js";
|
||||||
|
import type { PaseoToolingRuntime } from "./runtime.js";
|
||||||
|
|
||||||
|
const ToolExecuteBodySchema = z.object({
|
||||||
|
provider: z.string().min(1),
|
||||||
|
sessionId: z.string().min(1),
|
||||||
|
tool: z.string().min(1),
|
||||||
|
input: z.unknown().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface MountPaseoToolingHttpAdapterOptions {
|
||||||
|
app: express.Express;
|
||||||
|
registry: PaseoToolRegistry;
|
||||||
|
runtime: PaseoToolingRuntime;
|
||||||
|
logger: Logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mountPaseoToolingHttpAdapter(options: MountPaseoToolingHttpAdapterOptions): void {
|
||||||
|
const { app, registry, runtime, logger } = options;
|
||||||
|
const requireAuth: express.RequestHandler = (req, res, next) => {
|
||||||
|
if (req.header("authorization") === `Bearer ${runtime.token}`) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(401).json({ error: "Unauthorized" });
|
||||||
|
};
|
||||||
|
|
||||||
|
app.get("/api/paseo-tooling/manifest", requireAuth, (_req, res) => {
|
||||||
|
res.json({ tools: registry.listManifest() });
|
||||||
|
});
|
||||||
|
|
||||||
|
const runExecuteRequest = async (req: express.Request, res: express.Response): Promise<void> => {
|
||||||
|
const parsed = ToolExecuteBodySchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ error: parsed.error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const callerAgentId = runtime.resolveProviderSession({
|
||||||
|
provider: parsed.data.provider as AgentProvider,
|
||||||
|
sessionId: parsed.data.sessionId,
|
||||||
|
});
|
||||||
|
if (!callerAgentId) {
|
||||||
|
res.status(404).json({ error: "Provider session is not bound to a Paseo agent" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await registry.executeTool({
|
||||||
|
name: parsed.data.tool,
|
||||||
|
input: parsed.data.input ?? {},
|
||||||
|
callerAgentId,
|
||||||
|
});
|
||||||
|
res.json({ output: formatPaseoToolResult(result) });
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
{
|
||||||
|
err,
|
||||||
|
provider: parsed.data.provider,
|
||||||
|
sessionId: parsed.data.sessionId,
|
||||||
|
tool: parsed.data.tool,
|
||||||
|
},
|
||||||
|
"Paseo tooling HTTP execution failed",
|
||||||
|
);
|
||||||
|
res.status(500).json({ error: err instanceof Error ? err.message : "Tool execution failed" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
app.post("/api/paseo-tooling/execute", requireAuth, (req, res) => {
|
||||||
|
void runExecuteRequest(req, res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPaseoToolResult(result: unknown): unknown {
|
||||||
|
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const record = result as Record<string, unknown>;
|
||||||
|
if (record.structuredContent !== undefined) {
|
||||||
|
return record.structuredContent;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(record.content)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const text = record.content
|
||||||
|
.map((part) => {
|
||||||
|
if (!part || typeof part !== "object" || Array.isArray(part)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const value = (part as Record<string, unknown>).text;
|
||||||
|
return typeof value === "string" ? value : null;
|
||||||
|
})
|
||||||
|
.filter((part): part is string => part !== null)
|
||||||
|
.join("\n");
|
||||||
|
return text || record.content;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createPaseoToolRegistry,
|
||||||
|
type AgentMcpServerOptions,
|
||||||
|
type PaseoToolingRegistryOptions,
|
||||||
|
} from "./registry.js";
|
||||||
|
|
||||||
|
export async function createAgentMcpServer(options: AgentMcpServerOptions): Promise<McpServer> {
|
||||||
|
const server = new McpServer({
|
||||||
|
name: "agent-mcp",
|
||||||
|
version: "2.0.0",
|
||||||
|
});
|
||||||
|
const registry = createPaseoToolRegistry(options);
|
||||||
|
for (const tool of registry.listTools()) {
|
||||||
|
server.registerTool(tool.name, tool.config, tool.handler);
|
||||||
|
}
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { AgentMcpServerOptions, PaseoToolingRegistryOptions };
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import type { AgentLaunchContext, AgentSessionConfig } from "../agent-sdk-types.js";
|
||||||
|
|
||||||
|
const PASEO_MCP_SERVER_NAME = "paseo";
|
||||||
|
|
||||||
|
export function withPaseoToolingMcpServer(
|
||||||
|
config: AgentSessionConfig,
|
||||||
|
launchContext: AgentLaunchContext | undefined,
|
||||||
|
): AgentSessionConfig {
|
||||||
|
const mcpUrl = launchContext?.paseoTooling?.mcpUrl;
|
||||||
|
if (!mcpUrl || config.mcpServers?.[PASEO_MCP_SERVER_NAME]) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
mcpServers: {
|
||||||
|
[PASEO_MCP_SERVER_NAME]: {
|
||||||
|
type: "http",
|
||||||
|
url: mcpUrl,
|
||||||
|
},
|
||||||
|
...config.mcpServers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { mkdtempSync, rmSync } from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
||||||
|
import { createPaseoToolRegistry } from "./registry.js";
|
||||||
|
|
||||||
|
describe("Paseo tooling registry", () => {
|
||||||
|
test("executes tools with runtime caller identity", async () => {
|
||||||
|
const callerCwd = mkdtempSync(path.join(os.tmpdir(), "paseo-tooling-caller-"));
|
||||||
|
const requestedCwd = path.join(callerCwd, "child");
|
||||||
|
const createdTerminals: Array<{ cwd: string; name?: string }> = [];
|
||||||
|
const terminalManager = {
|
||||||
|
createTerminal: async (input: { cwd: string; name?: string }) => {
|
||||||
|
createdTerminals.push(input);
|
||||||
|
return { id: "terminal-1", name: input.name ?? "Terminal", cwd: input.cwd };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const agentManager = {
|
||||||
|
getAgent: (agentId: string) =>
|
||||||
|
agentId === "agent-1" ? { cwd: callerCwd, config: {} } : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const registry = createPaseoToolRegistry({
|
||||||
|
agentManager: agentManager as never,
|
||||||
|
agentStorage: {} as never,
|
||||||
|
terminalManager: terminalManager as never,
|
||||||
|
providerSnapshotManager: {} as never,
|
||||||
|
agentScopedTools: true,
|
||||||
|
logger: createTestLogger(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await registry.executeTool({
|
||||||
|
name: "create_terminal",
|
||||||
|
callerAgentId: "agent-1",
|
||||||
|
input: {
|
||||||
|
cwd: "child",
|
||||||
|
name: "Build",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createdTerminals).toEqual([{ cwd: requestedCwd, name: "Build" }]);
|
||||||
|
expect(result.structuredContent).toEqual({
|
||||||
|
id: "terminal-1",
|
||||||
|
name: "Build",
|
||||||
|
cwd: requestedCwd,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
rmSync(callerCwd, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
2583
packages/server/src/server/agent/paseo-tooling/registry.ts
Normal file
2583
packages/server/src/server/agent/paseo-tooling/registry.ts
Normal file
File diff suppressed because it is too large
Load Diff
61
packages/server/src/server/agent/paseo-tooling/runtime.ts
Normal file
61
packages/server/src/server/agent/paseo-tooling/runtime.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { randomBytes } from "node:crypto";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
AgentProvider,
|
||||||
|
PaseoToolingLaunchContext,
|
||||||
|
PaseoToolingProviderSessionRef,
|
||||||
|
} from "../agent-sdk-types.js";
|
||||||
|
|
||||||
|
interface PaseoToolingRuntimeEndpoints {
|
||||||
|
mcpBaseUrl: string | null;
|
||||||
|
httpBaseUrl: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PaseoToolingRuntime {
|
||||||
|
private mcpBaseUrl: string | null = null;
|
||||||
|
private httpBaseUrl: string | null = null;
|
||||||
|
private enabled = true;
|
||||||
|
private readonly sessionBindings = new Map<string, string>();
|
||||||
|
readonly token = randomBytes(32).toString("hex");
|
||||||
|
|
||||||
|
setEnabled(enabled: boolean): void {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEndpoints(endpoints: PaseoToolingRuntimeEndpoints): void {
|
||||||
|
this.mcpBaseUrl = endpoints.mcpBaseUrl;
|
||||||
|
this.httpBaseUrl = endpoints.httpBaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
createLaunchContext(agentId: string): PaseoToolingLaunchContext | undefined {
|
||||||
|
if (!this.enabled || (!this.mcpBaseUrl && !this.httpBaseUrl)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
agentId,
|
||||||
|
mcpUrl: this.mcpBaseUrl ? `${this.mcpBaseUrl}?callerAgentId=${agentId}` : null,
|
||||||
|
httpBaseUrl: this.httpBaseUrl,
|
||||||
|
token: this.token,
|
||||||
|
bindProviderSession: (ref) => this.bindProviderSession({ ...ref, agentId }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
bindProviderSession(ref: PaseoToolingProviderSessionRef & { agentId: string }): () => void {
|
||||||
|
const key = providerSessionKey(ref.provider, ref.sessionId);
|
||||||
|
this.sessionBindings.set(key, ref.agentId);
|
||||||
|
return () => {
|
||||||
|
if (this.sessionBindings.get(key) === ref.agentId) {
|
||||||
|
this.sessionBindings.delete(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveProviderSession(ref: PaseoToolingProviderSessionRef): string | null {
|
||||||
|
return this.sessionBindings.get(providerSessionKey(ref.provider, ref.sessionId)) ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function providerSessionKey(provider: AgentProvider, sessionId: string): string {
|
||||||
|
return `${provider}:${sessionId}`;
|
||||||
|
}
|
||||||
@@ -93,6 +93,7 @@ import {
|
|||||||
type ProviderRuntimeSettings,
|
type ProviderRuntimeSettings,
|
||||||
} from "../provider-launch-config.js";
|
} from "../provider-launch-config.js";
|
||||||
import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
|
import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
|
||||||
|
import { withPaseoToolingMcpServer } from "../paseo-tooling/provider-mcp.js";
|
||||||
import { appendOrReplaceGrowingAssistantMessage, runProviderTurn } from "./provider-runner.js";
|
import { appendOrReplaceGrowingAssistantMessage, runProviderTurn } from "./provider-runner.js";
|
||||||
import { platformShell, spawnProcess } from "../../../utils/spawn.js";
|
import { platformShell, spawnProcess } from "../../../utils/spawn.js";
|
||||||
|
|
||||||
@@ -596,29 +597,30 @@ export class ACPAgentClient implements AgentClient {
|
|||||||
launchContext?: AgentLaunchContext,
|
launchContext?: AgentLaunchContext,
|
||||||
): Promise<AgentSession> {
|
): Promise<AgentSession> {
|
||||||
this.assertProvider(config);
|
this.assertProvider(config);
|
||||||
const session = new ACPAgentSession(
|
const launchConfig = withPaseoToolingMcpServer(
|
||||||
{ ...config, provider: this.provider },
|
{ ...config, provider: this.provider },
|
||||||
{
|
launchContext,
|
||||||
provider: this.provider,
|
|
||||||
logger: this.logger,
|
|
||||||
runtimeSettings: this.runtimeSettings,
|
|
||||||
defaultCommand: this.defaultCommand,
|
|
||||||
defaultModes: this.defaultModes,
|
|
||||||
modelTransformer: this.modelTransformer,
|
|
||||||
sessionResponseTransformer: this.sessionResponseTransformer,
|
|
||||||
configOptionsTransformer: this.configOptionsTransformer,
|
|
||||||
modeIdTransformer: this.modeIdTransformer,
|
|
||||||
toolSnapshotTransformer: this.toolSnapshotTransformer,
|
|
||||||
providerModeWriter: this.providerModeWriter,
|
|
||||||
beforeModeWriter: this.beforeModeWriter,
|
|
||||||
thinkingOptionWriter: this.thinkingOptionWriter,
|
|
||||||
capabilities: this.capabilities,
|
|
||||||
agentId: launchContext?.agentId,
|
|
||||||
launchEnv: launchContext?.env,
|
|
||||||
waitForInitialCommands: this.waitForInitialCommands,
|
|
||||||
initialCommandsWaitTimeoutMs: this.initialCommandsWaitTimeoutMs,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
const session = new ACPAgentSession(launchConfig, {
|
||||||
|
provider: this.provider,
|
||||||
|
logger: this.logger,
|
||||||
|
runtimeSettings: this.runtimeSettings,
|
||||||
|
defaultCommand: this.defaultCommand,
|
||||||
|
defaultModes: this.defaultModes,
|
||||||
|
modelTransformer: this.modelTransformer,
|
||||||
|
sessionResponseTransformer: this.sessionResponseTransformer,
|
||||||
|
configOptionsTransformer: this.configOptionsTransformer,
|
||||||
|
modeIdTransformer: this.modeIdTransformer,
|
||||||
|
toolSnapshotTransformer: this.toolSnapshotTransformer,
|
||||||
|
providerModeWriter: this.providerModeWriter,
|
||||||
|
beforeModeWriter: this.beforeModeWriter,
|
||||||
|
thinkingOptionWriter: this.thinkingOptionWriter,
|
||||||
|
capabilities: this.capabilities,
|
||||||
|
agentId: launchContext?.agentId,
|
||||||
|
launchEnv: launchContext?.env,
|
||||||
|
waitForInitialCommands: this.waitForInitialCommands,
|
||||||
|
initialCommandsWaitTimeoutMs: this.initialCommandsWaitTimeoutMs,
|
||||||
|
});
|
||||||
await session.initializeNewSession();
|
await session.initializeNewSession();
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
@@ -638,12 +640,15 @@ export class ACPAgentClient implements AgentClient {
|
|||||||
throw new Error(`${this.provider} resume requires the original working directory`);
|
throw new Error(`${this.provider} resume requires the original working directory`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergedConfig: AgentSessionConfig = {
|
const mergedConfig = withPaseoToolingMcpServer(
|
||||||
...storedConfig,
|
{
|
||||||
...overrides,
|
...storedConfig,
|
||||||
provider: this.provider,
|
...overrides,
|
||||||
cwd,
|
provider: this.provider,
|
||||||
};
|
cwd,
|
||||||
|
},
|
||||||
|
launchContext,
|
||||||
|
);
|
||||||
const session = new ACPAgentSession(mergedConfig, {
|
const session = new ACPAgentSession(mergedConfig, {
|
||||||
provider: this.provider,
|
provider: this.provider,
|
||||||
logger: this.logger,
|
logger: this.logger,
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import { renderPromptAttachmentAsText } from "../../prompt-attachments.js";
|
|||||||
import { claudeQuery, type ClaudeOptions, type ClaudeQueryFactory } from "./query.js";
|
import { claudeQuery, type ClaudeOptions, type ClaudeQueryFactory } from "./query.js";
|
||||||
import { realClaudeRewindSdk, revertClaudeConversation, revertClaudeFiles } from "./rewind.js";
|
import { realClaudeRewindSdk, revertClaudeConversation, revertClaudeFiles } from "./rewind.js";
|
||||||
import { normalizeProviderReplayTimestamp } from "../../provider-history-timestamps.js";
|
import { normalizeProviderReplayTimestamp } from "../../provider-history-timestamps.js";
|
||||||
|
import { withPaseoToolingMcpServer } from "../../paseo-tooling/provider-mcp.js";
|
||||||
import { claudeProjectDirSync } from "./project-dir.js";
|
import { claudeProjectDirSync } from "./project-dir.js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -1297,7 +1298,7 @@ export class ClaudeAgentClient implements AgentClient {
|
|||||||
launchContext?: AgentLaunchContext,
|
launchContext?: AgentLaunchContext,
|
||||||
options?: AgentCreateSessionOptions,
|
options?: AgentCreateSessionOptions,
|
||||||
): Promise<AgentSession> {
|
): Promise<AgentSession> {
|
||||||
const claudeConfig = this.assertConfig(config);
|
const claudeConfig = this.assertConfig(withPaseoToolingMcpServer(config, launchContext));
|
||||||
return new ClaudeAgentSession(claudeConfig, {
|
return new ClaudeAgentSession(claudeConfig, {
|
||||||
defaults: this.defaults,
|
defaults: this.defaults,
|
||||||
runtimeSettings: this.runtimeSettings,
|
runtimeSettings: this.runtimeSettings,
|
||||||
@@ -1325,7 +1326,7 @@ export class ClaudeAgentClient implements AgentClient {
|
|||||||
provider: "claude",
|
provider: "claude",
|
||||||
cwd: merged.cwd,
|
cwd: merged.cwd,
|
||||||
};
|
};
|
||||||
const claudeConfig = this.assertConfig(mergedConfig);
|
const claudeConfig = this.assertConfig(withPaseoToolingMcpServer(mergedConfig, launchContext));
|
||||||
return new ClaudeAgentSession(claudeConfig, {
|
return new ClaudeAgentSession(claudeConfig, {
|
||||||
defaults: this.defaults,
|
defaults: this.defaults,
|
||||||
runtimeSettings: this.runtimeSettings,
|
runtimeSettings: this.runtimeSettings,
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ import {
|
|||||||
toDiagnosticErrorMessage,
|
toDiagnosticErrorMessage,
|
||||||
} from "./diagnostic-utils.js";
|
} from "./diagnostic-utils.js";
|
||||||
import { runProviderTurn } from "./provider-runner.js";
|
import { runProviderTurn } from "./provider-runner.js";
|
||||||
|
import { withPaseoToolingMcpServer } from "../paseo-tooling/provider-mcp.js";
|
||||||
import type { WorkspaceGitService } from "../../workspace-git-service.js";
|
import type { WorkspaceGitService } from "../../workspace-git-service.js";
|
||||||
|
|
||||||
function assertChildWithPipes(
|
function assertChildWithPipes(
|
||||||
@@ -5433,7 +5434,10 @@ export class CodexAppServerAgentClient implements AgentClient {
|
|||||||
// TODO: Honor persistSession=false if app-server adds support, or route
|
// TODO: Honor persistSession=false if app-server adds support, or route
|
||||||
// utility generations through `codex exec --ephemeral` in a larger change.
|
// utility generations through `codex exec --ephemeral` in a larger change.
|
||||||
}
|
}
|
||||||
const sessionConfig: AgentSessionConfig = { ...config, provider: CODEX_PROVIDER };
|
const sessionConfig = withPaseoToolingMcpServer(
|
||||||
|
{ ...config, provider: CODEX_PROVIDER },
|
||||||
|
launchContext,
|
||||||
|
);
|
||||||
const goalsEnabled = await this.resolveGoalsEnabled();
|
const goalsEnabled = await this.resolveGoalsEnabled();
|
||||||
const autoReviewEnabled = await this.resolveAutoReviewEnabled();
|
const autoReviewEnabled = await this.resolveAutoReviewEnabled();
|
||||||
const session = new CodexAppServerAgentSession(
|
const session = new CodexAppServerAgentSession(
|
||||||
@@ -5458,12 +5462,15 @@ export class CodexAppServerAgentClient implements AgentClient {
|
|||||||
launchContext?: AgentLaunchContext,
|
launchContext?: AgentLaunchContext,
|
||||||
): Promise<AgentSession> {
|
): Promise<AgentSession> {
|
||||||
const storedConfig = (handle.metadata ?? {}) as Partial<AgentSessionConfig>;
|
const storedConfig = (handle.metadata ?? {}) as Partial<AgentSessionConfig>;
|
||||||
const merged: AgentSessionConfig = {
|
const merged = withPaseoToolingMcpServer(
|
||||||
...storedConfig,
|
{
|
||||||
...overrides,
|
...storedConfig,
|
||||||
provider: CODEX_PROVIDER,
|
...overrides,
|
||||||
cwd: overrides?.cwd ?? storedConfig.cwd ?? process.cwd(),
|
provider: CODEX_PROVIDER,
|
||||||
};
|
cwd: overrides?.cwd ?? storedConfig.cwd ?? process.cwd(),
|
||||||
|
},
|
||||||
|
launchContext,
|
||||||
|
);
|
||||||
const goalsEnabled = await this.resolveGoalsEnabled();
|
const goalsEnabled = await this.resolveGoalsEnabled();
|
||||||
const autoReviewEnabled = await this.resolveAutoReviewEnabled();
|
const autoReviewEnabled = await this.resolveAutoReviewEnabled();
|
||||||
const session = new CodexAppServerAgentSession(
|
const session = new CodexAppServerAgentSession(
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
TestOpenCodeRuntime,
|
TestOpenCodeRuntime,
|
||||||
} from "./opencode/test-utils/test-opencode-runtime.js";
|
} from "./opencode/test-utils/test-opencode-runtime.js";
|
||||||
import type {
|
import type {
|
||||||
|
AgentLaunchContext,
|
||||||
AgentSessionConfig,
|
AgentSessionConfig,
|
||||||
AgentStreamEvent,
|
AgentStreamEvent,
|
||||||
ToolCallTimelineItem,
|
ToolCallTimelineItem,
|
||||||
@@ -852,6 +853,55 @@ describe("OpenCode adapter context-window normalization", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("OpenCode adapter startTurn error handling", () => {
|
describe("OpenCode adapter startTurn error handling", () => {
|
||||||
|
test("uses native Paseo tooling plugin instead of registering internal MCP", async () => {
|
||||||
|
const runtime = new TestOpenCodeRuntime();
|
||||||
|
const openCodeClient = new TestOpenCodeClient();
|
||||||
|
runtime.enqueueClient(openCodeClient);
|
||||||
|
const cwd = tmpCwd();
|
||||||
|
const client = new OpenCodeAgentClient(createTestLogger(), undefined, { runtime });
|
||||||
|
const boundSessions: string[] = [];
|
||||||
|
const unboundSessions: string[] = [];
|
||||||
|
const launchContext: AgentLaunchContext = {
|
||||||
|
agentId: "agent-1",
|
||||||
|
env: { PASEO_AGENT_ID: "agent-1" },
|
||||||
|
paseoTooling: {
|
||||||
|
agentId: "agent-1",
|
||||||
|
mcpUrl: "http://127.0.0.1:6767/mcp/agents?callerAgentId=agent-1",
|
||||||
|
httpBaseUrl: "http://127.0.0.1:6767",
|
||||||
|
token: "tool-token",
|
||||||
|
bindProviderSession: ({ provider, sessionId }) => {
|
||||||
|
expect(provider).toBe("opencode");
|
||||||
|
boundSessions.push(sessionId);
|
||||||
|
return () => unboundSessions.push(sessionId);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = await client.createSession(
|
||||||
|
{
|
||||||
|
provider: "opencode",
|
||||||
|
cwd,
|
||||||
|
},
|
||||||
|
launchContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
await collectTurnEvents(streamSession(session, "hello"));
|
||||||
|
|
||||||
|
expect(runtime.acquisitions[0]?.env?.PASEO_AGENT_ID).toBe("agent-1");
|
||||||
|
expect(runtime.acquisitions[0]?.env?.OPENCODE_CONFIG_CONTENT).toContain(
|
||||||
|
"paseo-tooling-plugin.mjs",
|
||||||
|
);
|
||||||
|
expect(openCodeClient.calls.mcpAdd).toEqual([]);
|
||||||
|
expect(boundSessions).toEqual(["session-1"]);
|
||||||
|
|
||||||
|
await session.close();
|
||||||
|
expect(unboundSessions).toEqual(["session-1"]);
|
||||||
|
} finally {
|
||||||
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("dynamically adds injected MCP servers without config-backed connect", async () => {
|
test("dynamically adds injected MCP servers without config-backed connect", async () => {
|
||||||
const runtime = new TestOpenCodeRuntime();
|
const runtime = new TestOpenCodeRuntime();
|
||||||
const openCodeClient = new TestOpenCodeClient();
|
const openCodeClient = new TestOpenCodeClient();
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ import {
|
|||||||
type OpenCodeRuntime,
|
type OpenCodeRuntime,
|
||||||
type OpenCodeServerAcquisition,
|
type OpenCodeServerAcquisition,
|
||||||
} from "./opencode/runtime.js";
|
} from "./opencode/runtime.js";
|
||||||
|
import {
|
||||||
|
bindOpenCodePaseoToolingSession,
|
||||||
|
ensureOpenCodePaseoToolingRuntime,
|
||||||
|
withOpenCodePaseoToolingEnv,
|
||||||
|
} from "./opencode/paseo-tooling.js";
|
||||||
import { normalizeProviderReplayTimestamp } from "../provider-history-timestamps.js";
|
import { normalizeProviderReplayTimestamp } from "../provider-history-timestamps.js";
|
||||||
import { revertOpenCodeConversationAndFiles } from "./opencode/rewind.js";
|
import { revertOpenCodeConversationAndFiles } from "./opencode/rewind.js";
|
||||||
|
|
||||||
@@ -1280,9 +1285,14 @@ export class OpenCodeAgentClient implements AgentClient {
|
|||||||
options?: AgentCreateSessionOptions,
|
options?: AgentCreateSessionOptions,
|
||||||
): Promise<AgentSession> {
|
): Promise<AgentSession> {
|
||||||
const openCodeConfig = this.assertConfig(config);
|
const openCodeConfig = this.assertConfig(config);
|
||||||
|
const paseoToolingRuntime = ensureOpenCodePaseoToolingRuntime();
|
||||||
const acquisition = await this.runtime.acquireServer({
|
const acquisition = await this.runtime.acquireServer({
|
||||||
force: false,
|
force: false,
|
||||||
env: launchContext?.env,
|
env: withOpenCodePaseoToolingEnv({
|
||||||
|
env: launchContext?.env,
|
||||||
|
launchContext,
|
||||||
|
runtime: paseoToolingRuntime,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
const { url } = acquisition.server;
|
const { url } = acquisition.server;
|
||||||
const client = this.runtime.createClient({
|
const client = this.runtime.createClient({
|
||||||
@@ -1307,6 +1317,10 @@ export class OpenCodeAgentClient implements AgentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.populateModelContextWindowCache(client, openCodeConfig.cwd);
|
await this.populateModelContextWindowCache(client, openCodeConfig.cwd);
|
||||||
|
const unbindPaseoToolingSession = bindOpenCodePaseoToolingSession({
|
||||||
|
launchContext,
|
||||||
|
sessionId: session.id,
|
||||||
|
});
|
||||||
|
|
||||||
return new OpenCodeAgentSession(
|
return new OpenCodeAgentSession(
|
||||||
openCodeConfig,
|
openCodeConfig,
|
||||||
@@ -1317,6 +1331,7 @@ export class OpenCodeAgentClient implements AgentClient {
|
|||||||
acquisition.release,
|
acquisition.release,
|
||||||
options?.persistSession,
|
options?.persistSession,
|
||||||
launchContext?.agentId,
|
launchContext?.agentId,
|
||||||
|
unbindPaseoToolingSession,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
acquisition.release();
|
acquisition.release();
|
||||||
@@ -1342,7 +1357,15 @@ export class OpenCodeAgentClient implements AgentClient {
|
|||||||
cwd,
|
cwd,
|
||||||
};
|
};
|
||||||
const openCodeConfig = this.assertConfig(config);
|
const openCodeConfig = this.assertConfig(config);
|
||||||
const acquisition = await this.runtime.acquireServer({ force: false });
|
const paseoToolingRuntime = ensureOpenCodePaseoToolingRuntime();
|
||||||
|
const acquisition = await this.runtime.acquireServer({
|
||||||
|
force: false,
|
||||||
|
env: withOpenCodePaseoToolingEnv({
|
||||||
|
env: launchContext?.env,
|
||||||
|
launchContext,
|
||||||
|
runtime: paseoToolingRuntime,
|
||||||
|
}),
|
||||||
|
});
|
||||||
const { url } = acquisition.server;
|
const { url } = acquisition.server;
|
||||||
const client = this.runtime.createClient({
|
const client = this.runtime.createClient({
|
||||||
baseUrl: url,
|
baseUrl: url,
|
||||||
@@ -1351,6 +1374,10 @@ export class OpenCodeAgentClient implements AgentClient {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.populateModelContextWindowCache(client, openCodeConfig.cwd);
|
await this.populateModelContextWindowCache(client, openCodeConfig.cwd);
|
||||||
|
const unbindPaseoToolingSession = bindOpenCodePaseoToolingSession({
|
||||||
|
launchContext,
|
||||||
|
sessionId: handle.sessionId,
|
||||||
|
});
|
||||||
|
|
||||||
return new OpenCodeAgentSession(
|
return new OpenCodeAgentSession(
|
||||||
openCodeConfig,
|
openCodeConfig,
|
||||||
@@ -1361,6 +1388,7 @@ export class OpenCodeAgentClient implements AgentClient {
|
|||||||
acquisition.release,
|
acquisition.release,
|
||||||
undefined,
|
undefined,
|
||||||
launchContext?.agentId,
|
launchContext?.agentId,
|
||||||
|
unbindPaseoToolingSession,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
acquisition.release();
|
acquisition.release();
|
||||||
@@ -2802,6 +2830,7 @@ class OpenCodeAgentSession implements AgentSession {
|
|||||||
releaseServer?: () => void,
|
releaseServer?: () => void,
|
||||||
persistSession = true,
|
persistSession = true,
|
||||||
private readonly agentId?: string,
|
private readonly agentId?: string,
|
||||||
|
private readonly unbindPaseoToolingSession?: () => void,
|
||||||
) {
|
) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.client = client;
|
this.client = client;
|
||||||
@@ -3574,6 +3603,7 @@ class OpenCodeAgentSession implements AgentSession {
|
|||||||
await this.deleteProviderSessionIfEphemeral();
|
await this.deleteProviderSessionIfEphemeral();
|
||||||
this.activeForegroundTurnId = null;
|
this.activeForegroundTurnId = null;
|
||||||
} finally {
|
} finally {
|
||||||
|
this.unbindPaseoToolingSession?.();
|
||||||
this.releaseServer?.();
|
this.releaseServer?.();
|
||||||
this.releaseServer = null;
|
this.releaseServer = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { mkdtempSync, rmSync } from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
|
|
||||||
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
|
import { ensureOpenCodePaseoToolingRuntime } from "./paseo-tooling.js";
|
||||||
|
|
||||||
|
describe("OpenCode Paseo tooling plugin", () => {
|
||||||
|
const originalPaseoHome = process.env.PASEO_HOME;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalPaseoHome === undefined) {
|
||||||
|
delete process.env.PASEO_HOME;
|
||||||
|
} else {
|
||||||
|
process.env.PASEO_HOME = originalPaseoHome;
|
||||||
|
}
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalizes OpenCode string arguments before executing Paseo tools", async () => {
|
||||||
|
const paseoHome = mkdtempSync(path.join(os.tmpdir(), "opencode-paseo-tooling-test-"));
|
||||||
|
process.env.PASEO_HOME = paseoHome;
|
||||||
|
const runtime = ensureOpenCodePaseoToolingRuntime();
|
||||||
|
const postedBodies: unknown[] = [];
|
||||||
|
const fetchMock = vi.fn(async (url: string | URL, init?: RequestInit) => {
|
||||||
|
const pathname = new URL(url.toString()).pathname;
|
||||||
|
if (pathname === "/api/paseo-tooling/manifest") {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
name: "list_agents",
|
||||||
|
description: "List agents",
|
||||||
|
inputJsonSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
includeArchived: { type: "boolean", default: false },
|
||||||
|
sinceHours: { type: "integer", default: 48 },
|
||||||
|
statuses: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string", enum: ["running", "idle"] },
|
||||||
|
},
|
||||||
|
limit: { type: "integer", default: 50 },
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
{ status: 200 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
postedBodies.push(JSON.parse(String(init?.body)) as unknown);
|
||||||
|
return new Response(JSON.stringify({ output: "ok" }), { status: 200 });
|
||||||
|
});
|
||||||
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const plugin = (await import(
|
||||||
|
`${pathToFileURL(runtime.pluginPath).href}?test=${Date.now()}`
|
||||||
|
)) as {
|
||||||
|
server: (
|
||||||
|
input: unknown,
|
||||||
|
options: { baseUrl: string; token: string },
|
||||||
|
) => Promise<{
|
||||||
|
tool: Record<
|
||||||
|
string,
|
||||||
|
{ execute: (args: unknown, context: { sessionID: string }) => Promise<string> }
|
||||||
|
>;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
const server = await plugin.server({}, { baseUrl: "http://127.0.0.1:6767", token: "t" });
|
||||||
|
|
||||||
|
await server.tool.paseo_list_agents?.execute(
|
||||||
|
{
|
||||||
|
includeArchived: "False",
|
||||||
|
sinceHours: "",
|
||||||
|
statuses: '["running"]',
|
||||||
|
limit: "5",
|
||||||
|
},
|
||||||
|
{ sessionID: "session-1" },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(postedBodies).toEqual([
|
||||||
|
{
|
||||||
|
provider: "opencode",
|
||||||
|
sessionId: "session-1",
|
||||||
|
tool: "list_agents",
|
||||||
|
input: {
|
||||||
|
includeArchived: false,
|
||||||
|
statuses: ["running"],
|
||||||
|
limit: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
rmSync(paseoHome, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
import { createRequire } from "node:module";
|
||||||
|
import { mkdirSync, writeFileSync } from "node:fs";
|
||||||
|
import { homedir } from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
|
|
||||||
|
import type { AgentLaunchContext } from "../../agent-sdk-types.js";
|
||||||
|
|
||||||
|
const PASEO_OPENCODE_PLUGIN_FILENAME = "paseo-tooling-plugin.mjs";
|
||||||
|
|
||||||
|
export interface OpenCodePaseoToolingRuntime {
|
||||||
|
pluginPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePaseoHome(): string {
|
||||||
|
return process.env.PASEO_HOME?.trim() || path.join(homedir(), ".paseo");
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRuntimeDirectory(): string {
|
||||||
|
return path.join(resolvePaseoHome(), "opencode");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureOpenCodePaseoToolingRuntime(): OpenCodePaseoToolingRuntime {
|
||||||
|
const dir = resolveRuntimeDirectory();
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
const pluginPath = path.join(dir, PASEO_OPENCODE_PLUGIN_FILENAME);
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const zodImportUrl = pathToFileURL(require.resolve("zod")).href;
|
||||||
|
writeFileSync(pluginPath, createOpenCodePaseoToolingPluginSource(zodImportUrl), "utf8");
|
||||||
|
return { pluginPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withOpenCodePaseoToolingEnv(params: {
|
||||||
|
env?: Record<string, string>;
|
||||||
|
launchContext?: AgentLaunchContext;
|
||||||
|
runtime: OpenCodePaseoToolingRuntime;
|
||||||
|
}): Record<string, string> | undefined {
|
||||||
|
const tooling = params.launchContext?.paseoTooling;
|
||||||
|
if (!tooling?.httpBaseUrl) {
|
||||||
|
return params.env;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginSpec = [
|
||||||
|
pathToFileURL(params.runtime.pluginPath).href,
|
||||||
|
{
|
||||||
|
baseUrl: tooling.httpBaseUrl,
|
||||||
|
token: tooling.token,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
...params.env,
|
||||||
|
OPENCODE_CONFIG_CONTENT: mergeOpenCodeConfigContent(
|
||||||
|
params.env?.OPENCODE_CONFIG_CONTENT,
|
||||||
|
pluginSpec,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bindOpenCodePaseoToolingSession(params: {
|
||||||
|
launchContext?: AgentLaunchContext;
|
||||||
|
sessionId: string;
|
||||||
|
}): (() => void) | undefined {
|
||||||
|
return params.launchContext?.paseoTooling?.bindProviderSession({
|
||||||
|
provider: "opencode",
|
||||||
|
sessionId: params.sessionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeOpenCodeConfigContent(existing: string | undefined, pluginSpec: unknown[]): string {
|
||||||
|
const base = parseConfigContent(existing);
|
||||||
|
const existingPlugins = Array.isArray(base.plugin) ? base.plugin : [];
|
||||||
|
const pluginUrl = pluginSpec[0];
|
||||||
|
const withoutExistingPaseoPlugin = existingPlugins.filter((entry) => {
|
||||||
|
const spec = Array.isArray(entry) ? entry[0] : entry;
|
||||||
|
return spec !== pluginUrl;
|
||||||
|
});
|
||||||
|
return JSON.stringify({
|
||||||
|
...base,
|
||||||
|
plugin: [...withoutExistingPaseoPlugin, pluginSpec],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseConfigContent(existing: string | undefined): Record<string, unknown> {
|
||||||
|
if (!existing?.trim()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(existing) as unknown;
|
||||||
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
||||||
|
? (parsed as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createOpenCodePaseoToolingPluginSource(zodImportUrl: string): string {
|
||||||
|
return `
|
||||||
|
import { z } from ${JSON.stringify(zodImportUrl)};
|
||||||
|
|
||||||
|
export async function server(_input, options) {
|
||||||
|
const manifestResponse = await fetch(new URL("/api/paseo-tooling/manifest", options.baseUrl), {
|
||||||
|
headers: { authorization: "Bearer " + options.token },
|
||||||
|
});
|
||||||
|
if (!manifestResponse.ok) {
|
||||||
|
throw new Error("Failed to load Paseo tooling manifest: " + manifestResponse.status);
|
||||||
|
}
|
||||||
|
const manifest = await manifestResponse.json();
|
||||||
|
const tools = {};
|
||||||
|
for (const item of manifest.tools ?? []) {
|
||||||
|
const id = "paseo_" + item.name;
|
||||||
|
tools[id] = {
|
||||||
|
description: item.description,
|
||||||
|
args: jsonSchemaObjectToZodShape(item.inputJsonSchema),
|
||||||
|
execute: async (args, context) => {
|
||||||
|
const input = normalizeJsonSchemaInput(args, item.inputJsonSchema);
|
||||||
|
const response = await fetch(new URL("/api/paseo-tooling/execute", options.baseUrl), {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
authorization: "Bearer " + options.token,
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
provider: "opencode",
|
||||||
|
sessionId: context.sessionID,
|
||||||
|
tool: item.name,
|
||||||
|
input,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const payload = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(payload.error ?? "Paseo tool failed: " + response.status);
|
||||||
|
}
|
||||||
|
return typeof payload.output === "string" ? payload.output : JSON.stringify(payload.output ?? payload, null, 2);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { tool: tools };
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonSchemaObjectToZodShape(schema) {
|
||||||
|
const properties = schema?.properties && typeof schema.properties === "object" ? schema.properties : {};
|
||||||
|
const required = new Set(Array.isArray(schema?.required) ? schema.required : []);
|
||||||
|
const shape = {};
|
||||||
|
for (const [key, value] of Object.entries(properties)) {
|
||||||
|
let parsed = jsonSchemaToZod(value);
|
||||||
|
if (!required.has(key)) parsed = parsed.optional();
|
||||||
|
shape[key] = parsed;
|
||||||
|
}
|
||||||
|
return shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonSchemaToZod(schema) {
|
||||||
|
if (!schema || typeof schema !== "object") return z.any();
|
||||||
|
if (schema.anyOf) return unionToZod(schema.anyOf);
|
||||||
|
if (schema.oneOf) return unionToZod(schema.oneOf);
|
||||||
|
if (schema.const !== undefined) return z.literal(schema.const);
|
||||||
|
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
||||||
|
if (schema.enum.length === 1) return z.literal(schema.enum[0]);
|
||||||
|
if (schema.enum.every((item) => typeof item === "string")) return z.enum(schema.enum);
|
||||||
|
return z.union(schema.enum.map((item) => z.literal(item)));
|
||||||
|
}
|
||||||
|
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
||||||
|
const nullable = types.includes("null");
|
||||||
|
const type = types.find((item) => item !== "null");
|
||||||
|
let result;
|
||||||
|
switch (type) {
|
||||||
|
case "string":
|
||||||
|
result = z.string();
|
||||||
|
if (typeof schema.minLength === "number") result = result.min(schema.minLength);
|
||||||
|
if (typeof schema.maxLength === "number") result = result.max(schema.maxLength);
|
||||||
|
break;
|
||||||
|
case "integer":
|
||||||
|
result = z.number().int();
|
||||||
|
if (typeof schema.minimum === "number") result = result.min(schema.minimum);
|
||||||
|
if (typeof schema.maximum === "number") result = result.max(schema.maximum);
|
||||||
|
break;
|
||||||
|
case "number":
|
||||||
|
result = z.number();
|
||||||
|
if (typeof schema.minimum === "number") result = result.min(schema.minimum);
|
||||||
|
if (typeof schema.maximum === "number") result = result.max(schema.maximum);
|
||||||
|
break;
|
||||||
|
case "boolean":
|
||||||
|
result = z.boolean();
|
||||||
|
break;
|
||||||
|
case "array":
|
||||||
|
result = z.array(jsonSchemaToZod(schema.items));
|
||||||
|
break;
|
||||||
|
case "object":
|
||||||
|
result = z.object(jsonSchemaObjectToZodShape(schema));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result = z.any();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (schema.description && typeof result.describe === "function") result = result.describe(schema.description);
|
||||||
|
return nullable ? result.nullable() : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeJsonSchemaInput(input, schema) {
|
||||||
|
if (!schema || typeof schema !== "object") return input;
|
||||||
|
|
||||||
|
if (schema.anyOf) return normalizeUnionInput(input, schema.anyOf);
|
||||||
|
if (schema.oneOf) return normalizeUnionInput(input, schema.oneOf);
|
||||||
|
|
||||||
|
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
||||||
|
const type = types.find((item) => item !== "null");
|
||||||
|
|
||||||
|
if (typeof input === "string" && !types.includes("string")) {
|
||||||
|
const trimmed = input.trim();
|
||||||
|
if (!trimmed || trimmed === "undefined" || trimmed === "null") return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "boolean":
|
||||||
|
if (typeof input === "string") {
|
||||||
|
const normalized = input.trim().toLowerCase();
|
||||||
|
if (normalized === "true" || normalized === "yes" || normalized === "1") return true;
|
||||||
|
if (normalized === "false" || normalized === "no" || normalized === "0") return false;
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
case "integer":
|
||||||
|
case "number":
|
||||||
|
if (typeof input === "string" && input.trim() !== "" && /^-?\\d+(\\.\\d+)?$/.test(input.trim())) {
|
||||||
|
return Number(input);
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
case "array": {
|
||||||
|
const value = parseJsonString(input);
|
||||||
|
if (!Array.isArray(value)) return value;
|
||||||
|
return value.map((item) => normalizeJsonSchemaInput(item, schema.items)).filter((item) => item !== undefined);
|
||||||
|
}
|
||||||
|
case "object": {
|
||||||
|
const value = parseJsonString(input);
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
||||||
|
const properties = schema.properties && typeof schema.properties === "object" ? schema.properties : {};
|
||||||
|
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
||||||
|
const normalized = {};
|
||||||
|
for (const [key, propertySchema] of Object.entries(properties)) {
|
||||||
|
if (!(key in value)) continue;
|
||||||
|
const next = normalizeJsonSchemaInput(value[key], propertySchema);
|
||||||
|
if (next !== undefined || required.has(key)) normalized[key] = next;
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeUnionInput(input, schemas) {
|
||||||
|
const preferred = schemas.find((item) => item?.type && item.type !== "null") ?? schemas[0];
|
||||||
|
return normalizeJsonSchemaInput(input, preferred);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJsonString(value) {
|
||||||
|
if (typeof value !== "string") return value;
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed || (!trimmed.startsWith("{") && !trimmed.startsWith("["))) return value;
|
||||||
|
try {
|
||||||
|
return JSON.parse(trimmed);
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unionToZod(items) {
|
||||||
|
const nonNull = items.filter((item) => item?.type !== "null");
|
||||||
|
const nullable = nonNull.length !== items.length;
|
||||||
|
const parsed = nonNull.map(jsonSchemaToZod);
|
||||||
|
const result = parsed.length === 0 ? z.null() : parsed.length === 1 ? parsed[0] : z.union(parsed);
|
||||||
|
return nullable ? result.nullable() : result;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
type PersistedAgentDescriptor,
|
type PersistedAgentDescriptor,
|
||||||
} from "../../agent-sdk-types.js";
|
} from "../../agent-sdk-types.js";
|
||||||
import { runProviderTurn } from "../provider-runner.js";
|
import { runProviderTurn } from "../provider-runner.js";
|
||||||
|
import { withPaseoToolingMcpServer } from "../../paseo-tooling/provider-mcp.js";
|
||||||
import {
|
import {
|
||||||
checkProviderLaunchAvailable,
|
checkProviderLaunchAvailable,
|
||||||
resolveProviderLaunch,
|
resolveProviderLaunch,
|
||||||
@@ -1838,18 +1839,19 @@ export class PiRpcAgentClient implements AgentClient {
|
|||||||
config: AgentSessionConfig,
|
config: AgentSessionConfig,
|
||||||
launchContext?: AgentLaunchContext,
|
launchContext?: AgentLaunchContext,
|
||||||
): Promise<AgentSession> {
|
): Promise<AgentSession> {
|
||||||
const mcpConfig = await this.prepareMcpConfig(config.cwd, config.mcpServers);
|
const launchConfig = withPaseoToolingMcpServer(config, launchContext);
|
||||||
|
const mcpConfig = await this.prepareMcpConfig(launchConfig.cwd, launchConfig.mcpServers);
|
||||||
const paseoExtension = createPiPaseoExtensionFile();
|
const paseoExtension = createPiPaseoExtensionFile();
|
||||||
let runtimeSession: PiRuntimeSession;
|
let runtimeSession: PiRuntimeSession;
|
||||||
try {
|
try {
|
||||||
runtimeSession = await this.runtime.startSession({
|
runtimeSession = await this.runtime.startSession({
|
||||||
cwd: config.cwd,
|
cwd: launchConfig.cwd,
|
||||||
model: config.model,
|
model: launchConfig.model,
|
||||||
thinkingOptionId:
|
thinkingOptionId:
|
||||||
normalizePiThinkingOption(config.thinkingOptionId) ?? DEFAULT_PI_THINKING_LEVEL,
|
normalizePiThinkingOption(launchConfig.thinkingOptionId) ?? DEFAULT_PI_THINKING_LEVEL,
|
||||||
systemPrompt: composeSystemPromptParts(
|
systemPrompt: composeSystemPromptParts(
|
||||||
config.systemPrompt,
|
launchConfig.systemPrompt,
|
||||||
config.daemonAppendSystemPrompt,
|
launchConfig.daemonAppendSystemPrompt,
|
||||||
),
|
),
|
||||||
env: launchContext?.env,
|
env: launchContext?.env,
|
||||||
mcpConfigPath: mcpConfig?.path,
|
mcpConfigPath: mcpConfig?.path,
|
||||||
@@ -1863,7 +1865,7 @@ export class PiRpcAgentClient implements AgentClient {
|
|||||||
try {
|
try {
|
||||||
return new PiRpcAgentSession({
|
return new PiRpcAgentSession({
|
||||||
runtimeSession,
|
runtimeSession,
|
||||||
config,
|
config: launchConfig,
|
||||||
initialState: await runtimeSession.getState(),
|
initialState: await runtimeSession.getState(),
|
||||||
capabilities: withPiMcpCapability(mcpConfig !== null),
|
capabilities: withPiMcpCapability(mcpConfig !== null),
|
||||||
cleanup: combineCleanup([mcpConfig?.cleanup, paseoExtension.cleanup]),
|
cleanup: combineCleanup([mcpConfig?.cleanup, paseoExtension.cleanup]),
|
||||||
@@ -1879,7 +1881,7 @@ export class PiRpcAgentClient implements AgentClient {
|
|||||||
async resumeSession(
|
async resumeSession(
|
||||||
handle: AgentPersistenceHandle,
|
handle: AgentPersistenceHandle,
|
||||||
overrides?: Partial<AgentSessionConfig>,
|
overrides?: Partial<AgentSessionConfig>,
|
||||||
_launchContext?: AgentLaunchContext,
|
launchContext?: AgentLaunchContext,
|
||||||
): Promise<AgentSession> {
|
): Promise<AgentSession> {
|
||||||
const sessionFile = handle.nativeHandle;
|
const sessionFile = handle.nativeHandle;
|
||||||
if (!sessionFile) {
|
if (!sessionFile) {
|
||||||
@@ -1889,7 +1891,8 @@ export class PiRpcAgentClient implements AgentClient {
|
|||||||
const persistenceMetadata = parsePersistenceMetadata(handle.metadata);
|
const persistenceMetadata = parsePersistenceMetadata(handle.metadata);
|
||||||
const resumeConfig = buildResumeConfig(persistenceMetadata, overrides);
|
const resumeConfig = buildResumeConfig(persistenceMetadata, overrides);
|
||||||
|
|
||||||
const mcpConfig = await this.prepareMcpConfig(resumeConfig.cwd, resumeConfig.config.mcpServers);
|
const launchConfig = withPaseoToolingMcpServer(resumeConfig.config, launchContext);
|
||||||
|
const mcpConfig = await this.prepareMcpConfig(resumeConfig.cwd, launchConfig.mcpServers);
|
||||||
const paseoExtension = createPiPaseoExtensionFile();
|
const paseoExtension = createPiPaseoExtensionFile();
|
||||||
let runtimeSession: PiRuntimeSession;
|
let runtimeSession: PiRuntimeSession;
|
||||||
try {
|
try {
|
||||||
@@ -1899,8 +1902,8 @@ export class PiRpcAgentClient implements AgentClient {
|
|||||||
model: resumeConfig.model,
|
model: resumeConfig.model,
|
||||||
thinkingOptionId: normalizePiThinkingOption(resumeConfig.thinkingOptionId) ?? undefined,
|
thinkingOptionId: normalizePiThinkingOption(resumeConfig.thinkingOptionId) ?? undefined,
|
||||||
systemPrompt: composeSystemPromptParts(
|
systemPrompt: composeSystemPromptParts(
|
||||||
resumeConfig.config.systemPrompt,
|
launchConfig.systemPrompt,
|
||||||
resumeConfig.config.daemonAppendSystemPrompt,
|
launchConfig.daemonAppendSystemPrompt,
|
||||||
),
|
),
|
||||||
mcpConfigPath: mcpConfig?.path,
|
mcpConfigPath: mcpConfig?.path,
|
||||||
extensionPaths: [paseoExtension.path],
|
extensionPaths: [paseoExtension.path],
|
||||||
@@ -1913,7 +1916,7 @@ export class PiRpcAgentClient implements AgentClient {
|
|||||||
try {
|
try {
|
||||||
return new PiRpcAgentSession({
|
return new PiRpcAgentSession({
|
||||||
runtimeSession,
|
runtimeSession,
|
||||||
config: resumeConfig.config,
|
config: launchConfig,
|
||||||
initialState: await runtimeSession.getState(),
|
initialState: await runtimeSession.getState(),
|
||||||
capabilities: withPiMcpCapability(mcpConfig !== null),
|
capabilities: withPiMcpCapability(mcpConfig !== null),
|
||||||
cleanup: combineCleanup([mcpConfig?.cleanup, paseoExtension.cleanup]),
|
cleanup: combineCleanup([mcpConfig?.cleanup, paseoExtension.cleanup]),
|
||||||
|
|||||||
@@ -26,28 +26,6 @@ export function stripInternalPaseoMcpServer(config: AgentSessionConfig): AgentSe
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withRuntimePaseoMcpServer(params: {
|
|
||||||
config: AgentSessionConfig;
|
|
||||||
agentId: string;
|
|
||||||
mcpBaseUrl: string | null;
|
|
||||||
}): AgentSessionConfig {
|
|
||||||
const storedConfig = stripInternalPaseoMcpServer(params.config);
|
|
||||||
if (!params.mcpBaseUrl || storedConfig.mcpServers?.[PASEO_MCP_SERVER_NAME]) {
|
|
||||||
return storedConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...storedConfig,
|
|
||||||
mcpServers: {
|
|
||||||
[PASEO_MCP_SERVER_NAME]: {
|
|
||||||
type: "http",
|
|
||||||
url: `${params.mcpBaseUrl}?callerAgentId=${params.agentId}`,
|
|
||||||
},
|
|
||||||
...storedConfig.mcpServers,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function isInternalPaseoMcpServer(config: McpServerConfig): boolean {
|
function isInternalPaseoMcpServer(config: McpServerConfig): boolean {
|
||||||
if (config.type !== "http" && config.type !== "sse") {
|
if (config.type !== "http" && config.type !== "sse") {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -99,6 +99,10 @@ import { AgentManager } from "./agent/agent-manager.js";
|
|||||||
import { AgentStorage } from "./agent/agent-storage.js";
|
import { AgentStorage } from "./agent/agent-storage.js";
|
||||||
import { attachAgentStoragePersistence } from "./persistence-hooks.js";
|
import { attachAgentStoragePersistence } from "./persistence-hooks.js";
|
||||||
import { createAgentMcpServer } from "./agent/mcp-server.js";
|
import { createAgentMcpServer } from "./agent/mcp-server.js";
|
||||||
|
import { createPaseoToolRegistry } from "./agent/paseo-tooling/registry.js";
|
||||||
|
import { mountPaseoToolingHttpAdapter } from "./agent/paseo-tooling/http-adapter.js";
|
||||||
|
import { PaseoToolingRuntime } from "./agent/paseo-tooling/runtime.js";
|
||||||
|
import type { PaseoToolingRegistryOptions } from "./agent/paseo-tooling/registry.js";
|
||||||
import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js";
|
import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js";
|
||||||
import { bootstrapWorkspaceRegistries } from "./workspace-registry-bootstrap.js";
|
import { bootstrapWorkspaceRegistries } from "./workspace-registry-bootstrap.js";
|
||||||
import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js";
|
import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js";
|
||||||
@@ -165,6 +169,14 @@ function createAgentMcpBaseUrl(listenTarget: ListenTarget | null): string | null
|
|||||||
).toString();
|
).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createDaemonHttpBaseUrl(listenTarget: ListenTarget | null): string | null {
|
||||||
|
if (!listenTarget || listenTarget.type !== "tcp") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const host = resolveAgentMcpClientHost(listenTarget.host);
|
||||||
|
return `http://${formatHostForHttpUrl(host)}:${listenTarget.port}`;
|
||||||
|
}
|
||||||
|
|
||||||
function summarizeAgentMcpDebugMessage(body: unknown): Record<string, unknown> {
|
function summarizeAgentMcpDebugMessage(body: unknown): Record<string, unknown> {
|
||||||
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
||||||
return {
|
return {
|
||||||
@@ -542,10 +554,13 @@ export async function createPaseoDaemon(
|
|||||||
extraClients: config.agentClients,
|
extraClients: config.agentClients,
|
||||||
});
|
});
|
||||||
const initialAgentManagerState = providerSnapshotManager.getAgentManagerProviderState();
|
const initialAgentManagerState = providerSnapshotManager.getAgentManagerProviderState();
|
||||||
|
const paseoToolingRuntime = new PaseoToolingRuntime();
|
||||||
|
paseoToolingRuntime.setEnabled(config.mcpInjectIntoAgents !== false);
|
||||||
const agentManager = new AgentManager({
|
const agentManager = new AgentManager({
|
||||||
clients: initialAgentManagerState.clients,
|
clients: initialAgentManagerState.clients,
|
||||||
providerDefinitions: initialAgentManagerState.providerDefinitions,
|
providerDefinitions: initialAgentManagerState.providerDefinitions,
|
||||||
registry: agentStorage,
|
registry: agentStorage,
|
||||||
|
paseoToolingRuntime,
|
||||||
appendSystemPrompt: config.appendSystemPrompt,
|
appendSystemPrompt: config.appendSystemPrompt,
|
||||||
logger,
|
logger,
|
||||||
});
|
});
|
||||||
@@ -689,79 +704,85 @@ export async function createPaseoDaemon(
|
|||||||
const agentMcpRoute = "/mcp/agents";
|
const agentMcpRoute = "/mcp/agents";
|
||||||
const agentMcpTransports: AgentMcpTransportMap = new Map();
|
const agentMcpTransports: AgentMcpTransportMap = new Map();
|
||||||
|
|
||||||
const createAgentMcpTransport = async (callerAgentId?: string) => {
|
const buildPaseoToolingRegistryOptions = (
|
||||||
const agentMcpServer = await createAgentMcpServer({
|
callerAgentId?: string,
|
||||||
agentManager,
|
): PaseoToolingRegistryOptions => ({
|
||||||
agentStorage,
|
agentManager,
|
||||||
terminalManager,
|
agentStorage,
|
||||||
getDaemonTcpPort: () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null),
|
terminalManager,
|
||||||
scheduleService,
|
getDaemonTcpPort: () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null),
|
||||||
providerSnapshotManager,
|
scheduleService,
|
||||||
github,
|
providerSnapshotManager,
|
||||||
workspaceGitService,
|
github,
|
||||||
archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
|
workspaceGitService,
|
||||||
emitWorkspaceUpdatesForWorkspaceIds: emitWorkspaceUpdatesExternal,
|
archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
|
||||||
markWorkspaceArchiving: markWorkspaceArchivingExternal,
|
emitWorkspaceUpdatesForWorkspaceIds: emitWorkspaceUpdatesExternal,
|
||||||
clearWorkspaceArchiving: clearWorkspaceArchivingExternal,
|
markWorkspaceArchiving: markWorkspaceArchivingExternal,
|
||||||
createPaseoWorktree: async (input, serviceOptions) => {
|
clearWorkspaceArchiving: clearWorkspaceArchivingExternal,
|
||||||
return createPaseoWorktreeWorkflow(
|
createPaseoWorktree: async (input, serviceOptions) => {
|
||||||
{
|
return createPaseoWorktreeWorkflow(
|
||||||
paseoHome: config.paseoHome,
|
{
|
||||||
worktreesRoot: config.worktreesRoot,
|
paseoHome: config.paseoHome,
|
||||||
createPaseoWorktree: async (workflowInput, workflowOptions) => {
|
worktreesRoot: config.worktreesRoot,
|
||||||
return createRegisteredPaseoWorktree(workflowInput, {
|
createPaseoWorktree: async (workflowInput, workflowOptions) => {
|
||||||
github,
|
return createRegisteredPaseoWorktree(workflowInput, {
|
||||||
...(workflowOptions?.resolveDefaultBranch
|
github,
|
||||||
? {
|
...(workflowOptions?.resolveDefaultBranch
|
||||||
resolveDefaultBranch: workflowOptions.resolveDefaultBranch,
|
? {
|
||||||
}
|
resolveDefaultBranch: workflowOptions.resolveDefaultBranch,
|
||||||
: {}),
|
}
|
||||||
projectRegistry,
|
: {}),
|
||||||
workspaceRegistry,
|
projectRegistry,
|
||||||
workspaceGitService,
|
workspaceRegistry,
|
||||||
});
|
workspaceGitService,
|
||||||
},
|
});
|
||||||
warmWorkspaceGitData: async (workspace) => {
|
|
||||||
await Promise.all(
|
|
||||||
wsServer
|
|
||||||
?.listActiveSessions()
|
|
||||||
.map((session) => session.warmWorkspaceGitDataForWorkspace(workspace)) ?? [],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
emitWorkspaceUpdateForCwd: async (cwd, emitOptions) => {
|
|
||||||
await Promise.all(
|
|
||||||
wsServer
|
|
||||||
?.listActiveSessions()
|
|
||||||
.map((session) => session.emitWorkspaceUpdatesForExternalCwds([cwd])) ?? [],
|
|
||||||
);
|
|
||||||
void emitOptions;
|
|
||||||
},
|
|
||||||
cacheWorkspaceSetupSnapshot: () => {},
|
|
||||||
emit: emitExternalSessionMessage,
|
|
||||||
sessionLogger: logger,
|
|
||||||
terminalManager,
|
|
||||||
archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
|
|
||||||
serviceProxy,
|
|
||||||
scriptRuntimeStore,
|
|
||||||
getDaemonTcpPort: () =>
|
|
||||||
boundListenTarget?.type === "tcp" ? boundListenTarget.port : null,
|
|
||||||
getDaemonTcpHost: () =>
|
|
||||||
boundListenTarget?.type === "tcp" ? boundListenTarget.host : null,
|
|
||||||
serviceProxyPublicBaseUrl,
|
|
||||||
onScriptsChanged: null,
|
|
||||||
},
|
},
|
||||||
input,
|
warmWorkspaceGitData: async (workspace) => {
|
||||||
serviceOptions,
|
await Promise.all(
|
||||||
);
|
wsServer
|
||||||
},
|
?.listActiveSessions()
|
||||||
paseoHome: config.paseoHome,
|
.map((session) => session.warmWorkspaceGitDataForWorkspace(workspace)) ?? [],
|
||||||
worktreesRoot: config.worktreesRoot,
|
);
|
||||||
callerAgentId,
|
},
|
||||||
enableVoiceTools: false,
|
emitWorkspaceUpdateForCwd: async (cwd, emitOptions) => {
|
||||||
resolveSpeakHandler: (agentId) => wsServer?.resolveVoiceSpeakHandler(agentId) ?? null,
|
await Promise.all(
|
||||||
resolveCallerContext: (agentId) => wsServer?.resolveVoiceCallerContext(agentId) ?? null,
|
wsServer
|
||||||
logger,
|
?.listActiveSessions()
|
||||||
});
|
.map((session) => session.emitWorkspaceUpdatesForExternalCwds([cwd])) ?? [],
|
||||||
|
);
|
||||||
|
void emitOptions;
|
||||||
|
},
|
||||||
|
cacheWorkspaceSetupSnapshot: () => {},
|
||||||
|
emit: emitExternalSessionMessage,
|
||||||
|
sessionLogger: logger,
|
||||||
|
terminalManager,
|
||||||
|
archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
|
||||||
|
serviceProxy,
|
||||||
|
scriptRuntimeStore,
|
||||||
|
getDaemonTcpPort: () =>
|
||||||
|
boundListenTarget?.type === "tcp" ? boundListenTarget.port : null,
|
||||||
|
getDaemonTcpHost: () =>
|
||||||
|
boundListenTarget?.type === "tcp" ? boundListenTarget.host : null,
|
||||||
|
serviceProxyPublicBaseUrl,
|
||||||
|
onScriptsChanged: null,
|
||||||
|
},
|
||||||
|
input,
|
||||||
|
serviceOptions,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
paseoHome: config.paseoHome,
|
||||||
|
worktreesRoot: config.worktreesRoot,
|
||||||
|
callerAgentId,
|
||||||
|
enableVoiceTools: false,
|
||||||
|
resolveSpeakHandler: (agentId) => wsServer?.resolveVoiceSpeakHandler(agentId) ?? null,
|
||||||
|
resolveCallerContext: (agentId) => wsServer?.resolveVoiceCallerContext(agentId) ?? null,
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createAgentMcpTransport = async (callerAgentId?: string) => {
|
||||||
|
const agentMcpServer = await createAgentMcpServer(
|
||||||
|
buildPaseoToolingRegistryOptions(callerAgentId),
|
||||||
|
);
|
||||||
|
|
||||||
const transport = new StreamableHTTPServerTransport({
|
const transport = new StreamableHTTPServerTransport({
|
||||||
sessionIdGenerator: () => randomUUID(),
|
sessionIdGenerator: () => randomUUID(),
|
||||||
@@ -870,6 +891,16 @@ export async function createPaseoDaemon(
|
|||||||
void runAgentMcpRequest(req, res);
|
void runAgentMcpRequest(req, res);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mountPaseoToolingHttpAdapter({
|
||||||
|
app,
|
||||||
|
registry: createPaseoToolRegistry({
|
||||||
|
...buildPaseoToolingRegistryOptions(),
|
||||||
|
agentScopedTools: true,
|
||||||
|
}),
|
||||||
|
runtime: paseoToolingRuntime,
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
|
||||||
app.post(agentMcpRoute, handleAgentMcpRequest);
|
app.post(agentMcpRoute, handleAgentMcpRequest);
|
||||||
app.get(agentMcpRoute, handleAgentMcpRequest);
|
app.get(agentMcpRoute, handleAgentMcpRequest);
|
||||||
app.delete(agentMcpRoute, handleAgentMcpRequest);
|
app.delete(agentMcpRoute, handleAgentMcpRequest);
|
||||||
@@ -917,10 +948,18 @@ export async function createPaseoDaemon(
|
|||||||
const logAndResolve = async () => {
|
const logAndResolve = async () => {
|
||||||
boundListenTarget = resolveBoundListenTarget(listenTarget, httpServer);
|
boundListenTarget = resolveBoundListenTarget(listenTarget, httpServer);
|
||||||
const mcpBaseUrl = mcpEnabled ? createAgentMcpBaseUrl(boundListenTarget) : null;
|
const mcpBaseUrl = mcpEnabled ? createAgentMcpBaseUrl(boundListenTarget) : null;
|
||||||
|
const httpBaseUrl = createDaemonHttpBaseUrl(boundListenTarget);
|
||||||
agentMcpBaseUrl = config.mcpInjectIntoAgents === false ? null : mcpBaseUrl;
|
agentMcpBaseUrl = config.mcpInjectIntoAgents === false ? null : mcpBaseUrl;
|
||||||
agentManager.setMcpBaseUrl(agentMcpBaseUrl);
|
paseoToolingRuntime.setEndpoints({
|
||||||
|
mcpBaseUrl: agentMcpBaseUrl,
|
||||||
|
httpBaseUrl: config.mcpInjectIntoAgents === false ? null : httpBaseUrl,
|
||||||
|
});
|
||||||
daemonConfigStore.onFieldChange("mcp.injectIntoAgents", (value) => {
|
daemonConfigStore.onFieldChange("mcp.injectIntoAgents", (value) => {
|
||||||
agentManager.setMcpBaseUrl(value ? mcpBaseUrl : null);
|
paseoToolingRuntime.setEnabled(value !== false);
|
||||||
|
paseoToolingRuntime.setEndpoints({
|
||||||
|
mcpBaseUrl: value ? mcpBaseUrl : null,
|
||||||
|
httpBaseUrl: value ? httpBaseUrl : null,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
daemonConfigStore.onFieldChange("appendSystemPrompt", (value) => {
|
daemonConfigStore.onFieldChange("appendSystemPrompt", (value) => {
|
||||||
agentManager.setAppendSystemPrompt(typeof value === "string" ? value : "");
|
agentManager.setAppendSystemPrompt(typeof value === "string" ? value : "");
|
||||||
|
|||||||
Reference in New Issue
Block a user