mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 07:04:48 +00:00
acp spec (#155)
This commit is contained in:
parent
70287ec471
commit
e72eb9f611
264 changed files with 18559 additions and 51021 deletions
|
|
@ -1168,26 +1168,6 @@
|
|||
width: auto;
|
||||
}
|
||||
|
||||
.mock-agent-hint {
|
||||
margin-top: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-2);
|
||||
border-radius: var(--radius);
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
max-width: 320px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.mock-agent-hint code {
|
||||
background: var(--border-2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Consolas, monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.empty-state-menu-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { ArrowLeft, ArrowRight, ChevronDown, ChevronRight, Pencil, Plus, X } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { AgentInfo, AgentModelInfo, AgentModeInfo, SkillSource } from "sandbox-agent";
|
||||
import type { McpServerEntry } from "../App";
|
||||
import type { AgentInfo, AgentModelInfo, AgentModeInfo, SkillSource } from "../types/legacyApi";
|
||||
|
||||
export type SessionConfig = {
|
||||
model: string;
|
||||
|
|
@ -14,8 +14,7 @@ const agentLabels: Record<string, string> = {
|
|||
claude: "Claude Code",
|
||||
codex: "Codex",
|
||||
opencode: "OpenCode",
|
||||
amp: "Amp",
|
||||
mock: "Mock"
|
||||
amp: "Amp"
|
||||
};
|
||||
|
||||
const validateServerJson = (json: string): string | null => {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ const badges = [
|
|||
type BadgeItem = (typeof badges)[number];
|
||||
|
||||
const getEnabled = (featureCoverage: FeatureCoverageView, key: BadgeItem["key"]) =>
|
||||
Boolean((featureCoverage as Record<string, boolean | undefined>)[key]);
|
||||
Boolean((featureCoverage as unknown as Record<string, boolean | undefined>)[key]);
|
||||
|
||||
const FeatureCoverageBadges = ({ featureCoverage }: { featureCoverage: FeatureCoverageView }) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -37,7 +37,9 @@ const ChatMessages = ({
|
|||
const isInProgress = item.status === "in_progress";
|
||||
const isFailed = item.status === "failed";
|
||||
const messageClass = getMessageClass(item);
|
||||
const statusLabel = item.status !== "completed" ? item.status.replace("_", " ") : "";
|
||||
const statusValue = item.status ?? "";
|
||||
const statusLabel =
|
||||
statusValue && statusValue !== "completed" ? statusValue.replace("_", " ") : "";
|
||||
const kindLabel = item.kind.replace("_", " ");
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import { MessageSquare, Plus, Square, Terminal } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { AgentInfo, AgentModelInfo, AgentModeInfo, PermissionEventData, QuestionEventData, SkillSource } from "sandbox-agent";
|
||||
import type { McpServerEntry } from "../../App";
|
||||
import type {
|
||||
AgentInfo,
|
||||
AgentModelInfo,
|
||||
AgentModeInfo,
|
||||
PermissionEventData,
|
||||
QuestionEventData,
|
||||
SkillSource
|
||||
} from "../../types/legacyApi";
|
||||
import ApprovalsTab from "../debug/ApprovalsTab";
|
||||
import SessionCreateMenu, { type SessionConfig } from "../SessionCreateMenu";
|
||||
import ChatInput from "./ChatInput";
|
||||
|
|
@ -175,11 +182,6 @@ const ChatPanel = ({
|
|||
<Terminal className="empty-state-icon" />
|
||||
<div className="empty-state-title">Ready to Chat</div>
|
||||
<p className="empty-state-text">Send a message to start a conversation with the agent.</p>
|
||||
{agentLabel === "Mock" && (
|
||||
<div className="mock-agent-hint">
|
||||
The mock agent simulates agent responses for testing the inspector UI without requiring API credentials. Send <code>help</code> for available commands.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<ChatMessages
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { UniversalItem } from "sandbox-agent";
|
||||
import type { UniversalItem } from "../../types/legacyApi";
|
||||
|
||||
export const getMessageClass = (item: UniversalItem) => {
|
||||
if (item.kind === "tool_call" || item.kind === "tool_result") return "tool";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ContentPart } from "sandbox-agent";
|
||||
import type { ContentPart } from "../../types/legacyApi";
|
||||
import { formatJson } from "../../utils/format";
|
||||
|
||||
const renderContentPart = (part: ContentPart, index: number) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { UniversalItem } from "sandbox-agent";
|
||||
import type { UniversalItem } from "../../types/legacyApi";
|
||||
|
||||
export type TimelineEntry = {
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Download, Loader2, RefreshCw } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { AgentInfo, AgentModeInfo } from "sandbox-agent";
|
||||
import type { AgentInfo, AgentModeInfo } from "../../types/legacyApi";
|
||||
import FeatureCoverageBadges from "../agents/FeatureCoverageBadges";
|
||||
import { emptyFeatureCoverage } from "../../types/agents";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { HelpCircle, Shield } from "lucide-react";
|
||||
import type { PermissionEventData, QuestionEventData } from "sandbox-agent";
|
||||
import type { PermissionEventData, QuestionEventData } from "../../types/legacyApi";
|
||||
import { formatJson } from "../../utils/format";
|
||||
|
||||
const ApprovalsTab = ({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Cloud, PlayCircle, Terminal } from "lucide-react";
|
||||
import type { AgentInfo, AgentModeInfo, UniversalEvent } from "sandbox-agent";
|
||||
import type { AgentInfo, AgentModeInfo, UniversalEvent } from "../../types/legacyApi";
|
||||
import AgentsTab from "./AgentsTab";
|
||||
import EventsTab from "./EventsTab";
|
||||
import RequestLogTab from "./RequestLogTab";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { UniversalEvent } from "sandbox-agent";
|
||||
import type { UniversalEvent } from "../../types/legacyApi";
|
||||
import { formatJson, formatTime } from "../../utils/format";
|
||||
import { getEventCategory, getEventClass, getEventIcon, getEventKey, getEventType } from "./eventUtils";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { Clipboard } from "lucide-react";
|
||||
import { ChevronDown, ChevronRight, Clipboard } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import type { RequestLog } from "../../types/requestLog";
|
||||
import { formatJson } from "../../utils/format";
|
||||
|
||||
const RequestLogTab = ({
|
||||
requestLog,
|
||||
|
|
@ -13,6 +15,12 @@ const RequestLogTab = ({
|
|||
onClear: () => void;
|
||||
onCopy: (entry: RequestLog) => void;
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState<Record<number, boolean>>({});
|
||||
|
||||
const toggleExpanded = (id: number) => {
|
||||
setExpanded((prev) => ({ ...prev, [id]: !prev[id] }));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="inline-row" style={{ marginBottom: 12, justifyContent: "space-between" }}>
|
||||
|
|
@ -25,28 +33,94 @@ const RequestLogTab = ({
|
|||
{requestLog.length === 0 ? (
|
||||
<div className="card-meta">No requests logged yet.</div>
|
||||
) : (
|
||||
requestLog.map((entry) => (
|
||||
<div key={entry.id} className="log-item">
|
||||
<span className="log-method">{entry.method}</span>
|
||||
<span className="log-url text-truncate">{entry.url}</span>
|
||||
<span className={`log-status ${entry.status && entry.status < 400 ? "ok" : "error"}`}>
|
||||
{entry.status || "ERR"}
|
||||
</span>
|
||||
<div className="log-meta">
|
||||
<span>
|
||||
{entry.time}
|
||||
{entry.error && ` - ${entry.error}`}
|
||||
</span>
|
||||
<button className="copy-button" onClick={() => onCopy(entry)}>
|
||||
<Clipboard />
|
||||
{copiedLogId === entry.id ? "Copied" : "curl"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
<div className="event-list">
|
||||
{requestLog.map((entry) => {
|
||||
const isExpanded = expanded[entry.id] ?? false;
|
||||
const hasDetails = entry.headers || entry.body || entry.responseBody;
|
||||
return (
|
||||
<div key={entry.id} className={`event-item ${isExpanded ? "expanded" : "collapsed"}`}>
|
||||
<button
|
||||
className="event-summary"
|
||||
type="button"
|
||||
onClick={() => hasDetails && toggleExpanded(entry.id)}
|
||||
title={hasDetails ? (isExpanded ? "Collapse" : "Expand") : undefined}
|
||||
style={{ cursor: hasDetails ? "pointer" : "default" }}
|
||||
>
|
||||
<div className="event-summary-main" style={{ flex: 1 }}>
|
||||
<div className="event-title-row">
|
||||
<span className="log-method">{entry.method}</span>
|
||||
<span className="log-url text-truncate" style={{ flex: 1 }}>{entry.url}</span>
|
||||
<span className={`log-status ${entry.status && entry.status < 400 ? "ok" : "error"}`}>
|
||||
{entry.status || "ERR"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="event-id">
|
||||
{entry.time}
|
||||
{entry.error && ` - ${entry.error}`}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className="copy-button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCopy(entry);
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.stopPropagation();
|
||||
onCopy(entry);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Clipboard size={14} />
|
||||
{copiedLogId === entry.id ? "Copied" : "curl"}
|
||||
</span>
|
||||
{hasDetails && (
|
||||
<span className="event-chevron">
|
||||
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="event-payload" style={{ padding: "8px 12px" }}>
|
||||
{entry.headers && Object.keys(entry.headers).length > 0 && (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<div className="part-title">Request Headers</div>
|
||||
<pre className="code-block">{Object.entries(entry.headers).map(([k, v]) => `${k}: ${v}`).join("\n")}</pre>
|
||||
</div>
|
||||
)}
|
||||
{entry.body && (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<div className="part-title">Request Body</div>
|
||||
<pre className="code-block">{formatJsonSafe(entry.body)}</pre>
|
||||
</div>
|
||||
)}
|
||||
{entry.responseBody && (
|
||||
<div>
|
||||
<div className="part-title">Response Body</div>
|
||||
<pre className="code-block">{formatJsonSafe(entry.responseBody)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const formatJsonSafe = (text: string): string => {
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
return formatJson(parsed);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
};
|
||||
|
||||
export default RequestLogTab;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
CheckCircle,
|
||||
FileDiff,
|
||||
HelpCircle,
|
||||
Info,
|
||||
MessageSquare,
|
||||
PauseCircle,
|
||||
PlayCircle,
|
||||
|
|
@ -13,7 +14,7 @@ import {
|
|||
Wrench,
|
||||
Zap
|
||||
} from "lucide-react";
|
||||
import type { UniversalEvent } from "sandbox-agent";
|
||||
import type { UniversalEvent } from "../../types/legacyApi";
|
||||
|
||||
export const getEventType = (event: UniversalEvent) => event.type;
|
||||
|
||||
|
|
@ -26,10 +27,45 @@ export const getEventClass = (type: string) => type.replace(/\./g, "-");
|
|||
|
||||
export const getEventIcon = (type: string) => {
|
||||
switch (type) {
|
||||
// ACP session update events
|
||||
case "acp.agent_message_chunk":
|
||||
return MessageSquare;
|
||||
case "acp.user_message_chunk":
|
||||
return MessageSquare;
|
||||
case "acp.agent_thought_chunk":
|
||||
return Brain;
|
||||
case "acp.tool_call":
|
||||
return Wrench;
|
||||
case "acp.tool_call_update":
|
||||
return Activity;
|
||||
case "acp.plan":
|
||||
return FileDiff;
|
||||
case "acp.session_info_update":
|
||||
return Info;
|
||||
case "acp.usage_update":
|
||||
return Info;
|
||||
case "acp.current_mode_update":
|
||||
return Info;
|
||||
case "acp.config_option_update":
|
||||
return Info;
|
||||
case "acp.available_commands_update":
|
||||
return Terminal;
|
||||
|
||||
// Inspector lifecycle events
|
||||
case "inspector.turn_started":
|
||||
return PlayCircle;
|
||||
case "inspector.turn_ended":
|
||||
return PauseCircle;
|
||||
case "inspector.user_message":
|
||||
return MessageSquare;
|
||||
|
||||
// Session lifecycle (inspector-emitted)
|
||||
case "session.started":
|
||||
return PlayCircle;
|
||||
case "session.ended":
|
||||
return PauseCircle;
|
||||
|
||||
// Legacy synthetic events
|
||||
case "turn.started":
|
||||
return PlayCircle;
|
||||
case "turn.ended":
|
||||
|
|
@ -40,6 +76,8 @@ export const getEventIcon = (type: string) => {
|
|||
return Activity;
|
||||
case "item.completed":
|
||||
return CheckCircle;
|
||||
|
||||
// Approval events
|
||||
case "question.requested":
|
||||
return HelpCircle;
|
||||
case "question.resolved":
|
||||
|
|
@ -48,11 +86,16 @@ export const getEventIcon = (type: string) => {
|
|||
return Shield;
|
||||
case "permission.resolved":
|
||||
return CheckCircle;
|
||||
|
||||
// Error events
|
||||
case "error":
|
||||
return AlertTriangle;
|
||||
case "agent.unparsed":
|
||||
return Brain;
|
||||
|
||||
default:
|
||||
if (type.startsWith("acp.")) return Zap;
|
||||
if (type.startsWith("inspector.")) return Info;
|
||||
if (type.startsWith("item.")) return MessageSquare;
|
||||
if (type.startsWith("session.")) return PlayCircle;
|
||||
if (type.startsWith("error")) return AlertTriangle;
|
||||
|
|
|
|||
790
frontend/packages/inspector/src/lib/legacyClient.ts
Normal file
790
frontend/packages/inspector/src/lib/legacyClient.ts
Normal file
|
|
@ -0,0 +1,790 @@
|
|||
import {
|
||||
SandboxAgent,
|
||||
type PermissionOption,
|
||||
type RequestPermissionRequest,
|
||||
type RequestPermissionResponse,
|
||||
type SandboxAgentAcpClient,
|
||||
type SandboxAgentConnectOptions,
|
||||
type SessionNotification,
|
||||
} from "sandbox-agent";
|
||||
import type {
|
||||
AgentInfo,
|
||||
AgentModelInfo,
|
||||
AgentModeInfo,
|
||||
AgentModelsResponse,
|
||||
AgentModesResponse,
|
||||
CreateSessionRequest,
|
||||
EventsQuery,
|
||||
EventsResponse,
|
||||
MessageRequest,
|
||||
PermissionEventData,
|
||||
PermissionReplyRequest,
|
||||
QuestionEventData,
|
||||
QuestionReplyRequest,
|
||||
SessionInfo,
|
||||
SessionListResponse,
|
||||
TurnStreamQuery,
|
||||
UniversalEvent,
|
||||
} from "../types/legacyApi";
|
||||
|
||||
type PendingPermission = {
|
||||
request: RequestPermissionRequest;
|
||||
resolve: (response: RequestPermissionResponse) => void;
|
||||
autoEndTurnOnResolve?: boolean;
|
||||
};
|
||||
|
||||
type PendingQuestion = {
|
||||
prompt: string;
|
||||
options: string[];
|
||||
autoEndTurnOnResolve?: boolean;
|
||||
};
|
||||
|
||||
type RuntimeSession = {
|
||||
aliasSessionId: string;
|
||||
realSessionId: string;
|
||||
agent: string;
|
||||
connection: SandboxAgentAcpClient;
|
||||
events: UniversalEvent[];
|
||||
nextSequence: number;
|
||||
listeners: Set<(event: UniversalEvent) => void>;
|
||||
info: SessionInfo;
|
||||
pendingPermissions: Map<string, PendingPermission>;
|
||||
pendingQuestions: Map<string, PendingQuestion>;
|
||||
};
|
||||
|
||||
const TDOO_PERMISSION_MODE =
|
||||
"TDOO: ACP permission mode preconfiguration is not implemented in inspector compatibility.";
|
||||
const TDOO_VARIANT =
|
||||
"TDOO: ACP session variants are not implemented in inspector compatibility.";
|
||||
const TDOO_SKILLS =
|
||||
"TDOO: ACP skills source configuration is not implemented in inspector compatibility.";
|
||||
const TDOO_MODE_DISCOVERY =
|
||||
"TDOO: ACP mode discovery before session creation is not implemented; returning cached/empty modes.";
|
||||
const TDOO_MODEL_DISCOVERY =
|
||||
"TDOO: ACP model discovery before session creation is not implemented; returning cached/empty models.";
|
||||
|
||||
export class InspectorLegacyClient {
|
||||
private readonly base: SandboxAgent;
|
||||
private readonly sessions = new Map<string, RuntimeSession>();
|
||||
private readonly aliasByRealSessionId = new Map<string, string>();
|
||||
private readonly modeCache = new Map<string, AgentModeInfo[]>();
|
||||
private readonly modelCache = new Map<string, AgentModelsResponse>();
|
||||
private permissionCounter = 0;
|
||||
|
||||
private constructor(base: SandboxAgent) {
|
||||
this.base = base;
|
||||
}
|
||||
|
||||
static async connect(options: SandboxAgentConnectOptions): Promise<InspectorLegacyClient> {
|
||||
const base = await SandboxAgent.connect(options);
|
||||
return new InspectorLegacyClient(base);
|
||||
}
|
||||
|
||||
async getHealth() {
|
||||
return this.base.getHealth();
|
||||
}
|
||||
|
||||
async listAgents(): Promise<{ agents: AgentInfo[] }> {
|
||||
const response = await this.base.listAgents();
|
||||
|
||||
return {
|
||||
agents: response.agents.map((agent) => {
|
||||
const installed =
|
||||
agent.agent_process_installed &&
|
||||
(!agent.native_required || agent.native_installed);
|
||||
return {
|
||||
id: agent.id,
|
||||
installed,
|
||||
credentialsAvailable: true,
|
||||
version: agent.agent_process_version ?? agent.native_version ?? null,
|
||||
path: null,
|
||||
capabilities: {
|
||||
unstable_methods: agent.capabilities.unstable_methods,
|
||||
},
|
||||
native_required: agent.native_required,
|
||||
native_installed: agent.native_installed,
|
||||
native_version: agent.native_version,
|
||||
agent_process_installed: agent.agent_process_installed,
|
||||
agent_process_source: agent.agent_process_source,
|
||||
agent_process_version: agent.agent_process_version,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async installAgent(agent: string, request: { reinstall?: boolean } = {}) {
|
||||
return this.base.installAgent(agent, request);
|
||||
}
|
||||
|
||||
async getAgentModes(agentId: string): Promise<AgentModesResponse> {
|
||||
const modes = this.modeCache.get(agentId);
|
||||
if (modes) {
|
||||
return { modes };
|
||||
}
|
||||
|
||||
console.warn(TDOO_MODE_DISCOVERY);
|
||||
return { modes: [] };
|
||||
}
|
||||
|
||||
async getAgentModels(agentId: string): Promise<AgentModelsResponse> {
|
||||
const models = this.modelCache.get(agentId);
|
||||
if (models) {
|
||||
return models;
|
||||
}
|
||||
|
||||
console.warn(TDOO_MODEL_DISCOVERY);
|
||||
return { models: [], defaultModel: null };
|
||||
}
|
||||
|
||||
async createSession(aliasSessionId: string, request: CreateSessionRequest): Promise<void> {
|
||||
await this.terminateSession(aliasSessionId).catch(() => {
|
||||
// Ignore if it doesn't exist yet.
|
||||
});
|
||||
|
||||
const acp = await this.base.createAcpClient({
|
||||
agent: request.agent,
|
||||
client: {
|
||||
sessionUpdate: async (notification) => {
|
||||
this.handleSessionUpdate(notification);
|
||||
},
|
||||
requestPermission: async (permissionRequest) => {
|
||||
return this.handlePermissionRequest(permissionRequest);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await acp.initialize();
|
||||
|
||||
const created = await acp.newSession({
|
||||
cwd: "/",
|
||||
mcpServers: convertMcpConfig(request.mcp ?? {}),
|
||||
});
|
||||
|
||||
if (created.modes?.availableModes) {
|
||||
this.modeCache.set(
|
||||
request.agent,
|
||||
created.modes.availableModes.map((mode) => ({
|
||||
id: mode.id,
|
||||
name: mode.name,
|
||||
description: mode.description ?? undefined,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
if (created.models?.availableModels) {
|
||||
this.modelCache.set(request.agent, {
|
||||
models: created.models.availableModels.map((model) => ({
|
||||
id: model.modelId,
|
||||
name: model.name,
|
||||
description: model.description ?? undefined,
|
||||
})),
|
||||
defaultModel: created.models.currentModelId ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
const runtime: RuntimeSession = {
|
||||
aliasSessionId,
|
||||
realSessionId: created.sessionId,
|
||||
agent: request.agent,
|
||||
connection: acp,
|
||||
events: [],
|
||||
nextSequence: 1,
|
||||
listeners: new Set(),
|
||||
info: {
|
||||
sessionId: aliasSessionId,
|
||||
agent: request.agent,
|
||||
eventCount: 0,
|
||||
ended: false,
|
||||
model: request.model ?? null,
|
||||
variant: request.variant ?? null,
|
||||
permissionMode: request.permissionMode ?? null,
|
||||
mcp: request.mcp,
|
||||
skills: request.skills,
|
||||
},
|
||||
pendingPermissions: new Map(),
|
||||
pendingQuestions: new Map(),
|
||||
};
|
||||
|
||||
this.sessions.set(aliasSessionId, runtime);
|
||||
this.aliasByRealSessionId.set(created.sessionId, aliasSessionId);
|
||||
|
||||
if (request.agentMode) {
|
||||
try {
|
||||
await acp.setSessionMode({ sessionId: created.sessionId, modeId: request.agentMode });
|
||||
} catch {
|
||||
this.emitError(aliasSessionId, `TDOO: Unable to apply mode \"${request.agentMode}\" via ACP.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (request.model) {
|
||||
try {
|
||||
await acp.unstableSetSessionModel({
|
||||
sessionId: created.sessionId,
|
||||
modelId: request.model,
|
||||
});
|
||||
} catch {
|
||||
this.emitError(aliasSessionId, `TDOO: Unable to apply model \"${request.model}\" via ACP.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (request.permissionMode) {
|
||||
this.emitError(aliasSessionId, TDOO_PERMISSION_MODE);
|
||||
}
|
||||
|
||||
if (request.variant) {
|
||||
this.emitError(aliasSessionId, TDOO_VARIANT);
|
||||
}
|
||||
|
||||
if (request.skills?.sources && request.skills.sources.length > 0) {
|
||||
this.emitError(aliasSessionId, TDOO_SKILLS);
|
||||
}
|
||||
|
||||
this.emitEvent(aliasSessionId, "session.started", {
|
||||
session_id: aliasSessionId,
|
||||
agent: request.agent,
|
||||
});
|
||||
}
|
||||
|
||||
async listSessions(): Promise<SessionListResponse> {
|
||||
const sessions = Array.from(this.sessions.values()).map((session) => {
|
||||
return {
|
||||
...session.info,
|
||||
eventCount: session.events.length,
|
||||
};
|
||||
});
|
||||
|
||||
return { sessions };
|
||||
}
|
||||
|
||||
async postMessage(sessionId: string, request: MessageRequest): Promise<void> {
|
||||
const runtime = this.requireActiveSession(sessionId);
|
||||
const message = request.message.trim();
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emitEvent(sessionId, "inspector.turn_started", {
|
||||
session_id: sessionId,
|
||||
});
|
||||
|
||||
this.emitEvent(sessionId, "inspector.user_message", {
|
||||
session_id: sessionId,
|
||||
text: message,
|
||||
});
|
||||
|
||||
try {
|
||||
await runtime.connection.prompt({
|
||||
sessionId: runtime.realSessionId,
|
||||
prompt: [{ type: "text", text: message }],
|
||||
});
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : "prompt failed";
|
||||
this.emitError(sessionId, detail);
|
||||
throw error;
|
||||
} finally {
|
||||
this.emitEvent(sessionId, "inspector.turn_ended", {
|
||||
session_id: sessionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getEvents(sessionId: string, query: EventsQuery = {}): Promise<EventsResponse> {
|
||||
const runtime = this.requireSession(sessionId);
|
||||
const offset = query.offset ?? 0;
|
||||
const limit = query.limit ?? 200;
|
||||
|
||||
const events = runtime.events.filter((event) => event.sequence > offset).slice(0, limit);
|
||||
return { events };
|
||||
}
|
||||
|
||||
async *streamEvents(
|
||||
sessionId: string,
|
||||
query: EventsQuery = {},
|
||||
signal?: AbortSignal,
|
||||
): AsyncIterable<UniversalEvent> {
|
||||
const runtime = this.requireSession(sessionId);
|
||||
let cursor = query.offset ?? 0;
|
||||
|
||||
for (const event of runtime.events) {
|
||||
if (event.sequence <= cursor) {
|
||||
continue;
|
||||
}
|
||||
cursor = event.sequence;
|
||||
yield event;
|
||||
}
|
||||
|
||||
const queue: UniversalEvent[] = [];
|
||||
let wake: (() => void) | null = null;
|
||||
|
||||
const listener = (event: UniversalEvent) => {
|
||||
if (event.sequence <= cursor) {
|
||||
return;
|
||||
}
|
||||
queue.push(event);
|
||||
if (wake) {
|
||||
wake();
|
||||
wake = null;
|
||||
}
|
||||
};
|
||||
|
||||
runtime.listeners.add(listener);
|
||||
|
||||
try {
|
||||
while (!signal?.aborted) {
|
||||
if (queue.length === 0) {
|
||||
await waitForSignalOrEvent(signal, () => {
|
||||
wake = () => {};
|
||||
return new Promise<void>((resolve) => {
|
||||
wake = resolve;
|
||||
});
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const next = queue.shift();
|
||||
if (!next) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cursor = next.sequence;
|
||||
yield next;
|
||||
}
|
||||
} finally {
|
||||
runtime.listeners.delete(listener);
|
||||
}
|
||||
}
|
||||
|
||||
async *streamTurn(
|
||||
sessionId: string,
|
||||
request: MessageRequest,
|
||||
_query?: TurnStreamQuery,
|
||||
signal?: AbortSignal,
|
||||
): AsyncIterable<UniversalEvent> {
|
||||
if (signal?.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const runtime = this.requireActiveSession(sessionId);
|
||||
let cursor = runtime.nextSequence - 1;
|
||||
const queue: UniversalEvent[] = [];
|
||||
let wake: (() => void) | null = null;
|
||||
let promptDone = false;
|
||||
let promptError: unknown = null;
|
||||
|
||||
const notify = () => {
|
||||
if (wake) {
|
||||
wake();
|
||||
wake = null;
|
||||
}
|
||||
};
|
||||
|
||||
const listener = (event: UniversalEvent) => {
|
||||
if (event.sequence <= cursor) {
|
||||
return;
|
||||
}
|
||||
queue.push(event);
|
||||
notify();
|
||||
};
|
||||
|
||||
runtime.listeners.add(listener);
|
||||
|
||||
const promptPromise = this.postMessage(sessionId, request)
|
||||
.catch((error) => {
|
||||
promptError = error;
|
||||
})
|
||||
.finally(() => {
|
||||
promptDone = true;
|
||||
notify();
|
||||
});
|
||||
|
||||
try {
|
||||
while (!signal?.aborted) {
|
||||
if (queue.length === 0) {
|
||||
if (promptDone) {
|
||||
break;
|
||||
}
|
||||
|
||||
await waitForSignalOrEvent(signal, () => {
|
||||
wake = () => {};
|
||||
return new Promise<void>((resolve) => {
|
||||
wake = resolve;
|
||||
});
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const next = queue.shift();
|
||||
if (!next) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cursor = next.sequence;
|
||||
yield next;
|
||||
}
|
||||
} finally {
|
||||
runtime.listeners.delete(listener);
|
||||
}
|
||||
|
||||
await promptPromise;
|
||||
if (promptError) {
|
||||
throw promptError;
|
||||
}
|
||||
}
|
||||
|
||||
async replyQuestion(
|
||||
sessionId: string,
|
||||
questionId: string,
|
||||
request: QuestionReplyRequest,
|
||||
): Promise<void> {
|
||||
const runtime = this.requireSession(sessionId);
|
||||
const pending = runtime.pendingQuestions.get(questionId);
|
||||
if (!pending) {
|
||||
throw new Error("TDOO: Question request no longer pending.");
|
||||
}
|
||||
|
||||
runtime.pendingQuestions.delete(questionId);
|
||||
const response = request.answers?.[0]?.[0] ?? null;
|
||||
const resolved: QuestionEventData & { response?: string | null } = {
|
||||
question_id: questionId,
|
||||
status: "resolved",
|
||||
prompt: pending.prompt,
|
||||
options: pending.options,
|
||||
response,
|
||||
};
|
||||
this.emitEvent(sessionId, "question.resolved", resolved);
|
||||
if (pending.autoEndTurnOnResolve) {
|
||||
this.emitEvent(sessionId, "turn.ended", { session_id: sessionId });
|
||||
}
|
||||
}
|
||||
|
||||
async rejectQuestion(sessionId: string, questionId: string): Promise<void> {
|
||||
const runtime = this.requireSession(sessionId);
|
||||
const pending = runtime.pendingQuestions.get(questionId);
|
||||
if (!pending) {
|
||||
throw new Error("TDOO: Question request no longer pending.");
|
||||
}
|
||||
|
||||
runtime.pendingQuestions.delete(questionId);
|
||||
const resolved: QuestionEventData & { response?: string | null } = {
|
||||
question_id: questionId,
|
||||
status: "resolved",
|
||||
prompt: pending.prompt,
|
||||
options: pending.options,
|
||||
response: null,
|
||||
};
|
||||
this.emitEvent(sessionId, "question.resolved", resolved);
|
||||
if (pending.autoEndTurnOnResolve) {
|
||||
this.emitEvent(sessionId, "turn.ended", { session_id: sessionId });
|
||||
}
|
||||
}
|
||||
|
||||
async replyPermission(
|
||||
sessionId: string,
|
||||
permissionId: string,
|
||||
request: PermissionReplyRequest,
|
||||
): Promise<void> {
|
||||
const runtime = this.requireSession(sessionId);
|
||||
const pending = runtime.pendingPermissions.get(permissionId);
|
||||
if (!pending) {
|
||||
throw new Error("TDOO: Permission request no longer pending.");
|
||||
}
|
||||
|
||||
const optionId = selectPermissionOption(pending.request.options, request.reply);
|
||||
const response: RequestPermissionResponse = optionId
|
||||
? {
|
||||
outcome: {
|
||||
outcome: "selected",
|
||||
optionId,
|
||||
},
|
||||
}
|
||||
: {
|
||||
outcome: {
|
||||
outcome: "cancelled",
|
||||
},
|
||||
};
|
||||
|
||||
pending.resolve(response);
|
||||
runtime.pendingPermissions.delete(permissionId);
|
||||
|
||||
const action = pending.request.toolCall.title ?? pending.request.toolCall.kind ?? "permission";
|
||||
const resolved: PermissionEventData = {
|
||||
permission_id: permissionId,
|
||||
status: "resolved",
|
||||
action,
|
||||
metadata: {
|
||||
reply: request.reply,
|
||||
},
|
||||
};
|
||||
|
||||
this.emitEvent(sessionId, "permission.resolved", resolved);
|
||||
if (pending.autoEndTurnOnResolve) {
|
||||
this.emitEvent(sessionId, "turn.ended", { session_id: sessionId });
|
||||
}
|
||||
}
|
||||
|
||||
async terminateSession(sessionId: string): Promise<void> {
|
||||
const runtime = this.sessions.get(sessionId);
|
||||
if (!runtime) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emitEvent(sessionId, "session.ended", {
|
||||
reason: "terminated_by_user",
|
||||
terminated_by: "inspector",
|
||||
});
|
||||
|
||||
runtime.info.ended = true;
|
||||
|
||||
for (const pending of runtime.pendingPermissions.values()) {
|
||||
pending.resolve({
|
||||
outcome: {
|
||||
outcome: "cancelled",
|
||||
},
|
||||
});
|
||||
}
|
||||
runtime.pendingPermissions.clear();
|
||||
runtime.pendingQuestions.clear();
|
||||
|
||||
try {
|
||||
await runtime.connection.close();
|
||||
} catch {
|
||||
// Best-effort close.
|
||||
}
|
||||
|
||||
this.aliasByRealSessionId.delete(runtime.realSessionId);
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
for (const sessionId of Array.from(this.sessions.keys())) {
|
||||
await this.terminateSession(sessionId);
|
||||
}
|
||||
|
||||
await this.base.dispose();
|
||||
}
|
||||
|
||||
private handleSessionUpdate(notification: SessionNotification): void {
|
||||
const aliasSessionId = this.aliasByRealSessionId.get(notification.sessionId);
|
||||
if (!aliasSessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const runtime = this.sessions.get(aliasSessionId);
|
||||
if (!runtime || runtime.info.ended) {
|
||||
return;
|
||||
}
|
||||
|
||||
const update = notification.update;
|
||||
|
||||
// Still handle session_info_update for sidebar metadata
|
||||
if (update.sessionUpdate === "session_info_update") {
|
||||
runtime.info.title = update.title ?? runtime.info.title;
|
||||
runtime.info.updatedAt = update.updatedAt ?? runtime.info.updatedAt;
|
||||
}
|
||||
|
||||
// Emit the raw notification as the event data, using the ACP discriminator as the type
|
||||
this.emitEvent(aliasSessionId, `acp.${update.sessionUpdate}`, notification);
|
||||
}
|
||||
|
||||
private async handlePermissionRequest(
|
||||
request: RequestPermissionRequest,
|
||||
): Promise<RequestPermissionResponse> {
|
||||
const aliasSessionId = this.aliasByRealSessionId.get(request.sessionId);
|
||||
if (!aliasSessionId) {
|
||||
return {
|
||||
outcome: {
|
||||
outcome: "cancelled",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const runtime = this.sessions.get(aliasSessionId);
|
||||
if (!runtime || runtime.info.ended) {
|
||||
return {
|
||||
outcome: {
|
||||
outcome: "cancelled",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
this.permissionCounter += 1;
|
||||
const permissionId = `permission-${this.permissionCounter}`;
|
||||
|
||||
const action = request.toolCall.title ?? request.toolCall.kind ?? "permission";
|
||||
const pendingEvent: PermissionEventData = {
|
||||
permission_id: permissionId,
|
||||
status: "requested",
|
||||
action,
|
||||
metadata: request,
|
||||
};
|
||||
|
||||
this.emitEvent(aliasSessionId, "permission.requested", pendingEvent);
|
||||
|
||||
return await new Promise<RequestPermissionResponse>((resolve) => {
|
||||
runtime.pendingPermissions.set(permissionId, { request, resolve });
|
||||
});
|
||||
}
|
||||
|
||||
private emitError(sessionId: string, message: string): void {
|
||||
this.emitEvent(sessionId, "error", {
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
private emitEvent(sessionId: string, type: string, data: unknown): void {
|
||||
const runtime = this.sessions.get(sessionId);
|
||||
if (!runtime) {
|
||||
return;
|
||||
}
|
||||
|
||||
const event: UniversalEvent = {
|
||||
event_id: `${sessionId}-${runtime.nextSequence}`,
|
||||
sequence: runtime.nextSequence,
|
||||
type,
|
||||
source: "inspector.acp",
|
||||
time: new Date().toISOString(),
|
||||
synthetic: true,
|
||||
data,
|
||||
};
|
||||
|
||||
runtime.nextSequence += 1;
|
||||
runtime.events.push(event);
|
||||
runtime.info.eventCount = runtime.events.length;
|
||||
|
||||
for (const listener of runtime.listeners) {
|
||||
listener(event);
|
||||
}
|
||||
}
|
||||
|
||||
private requireSession(sessionId: string): RuntimeSession {
|
||||
const runtime = this.sessions.get(sessionId);
|
||||
if (!runtime) {
|
||||
throw new Error(`Session not found: ${sessionId}`);
|
||||
}
|
||||
return runtime;
|
||||
}
|
||||
|
||||
private requireActiveSession(sessionId: string): RuntimeSession {
|
||||
const runtime = this.requireSession(sessionId);
|
||||
if (runtime.info.ended) {
|
||||
throw new Error(`Session ended: ${sessionId}`);
|
||||
}
|
||||
return runtime;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const convertMcpConfig = (mcp: Record<string, unknown>) => {
|
||||
return Object.entries(mcp)
|
||||
.map(([name, config]) => {
|
||||
if (!config || typeof config !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = config as Record<string, unknown>;
|
||||
const type = value.type;
|
||||
|
||||
if (type === "local") {
|
||||
const commandValue = value.command;
|
||||
const argsValue = value.args;
|
||||
|
||||
let command = "";
|
||||
let args: string[] = [];
|
||||
|
||||
if (Array.isArray(commandValue) && commandValue.length > 0) {
|
||||
command = String(commandValue[0] ?? "");
|
||||
args = commandValue.slice(1).map((part) => String(part));
|
||||
} else if (typeof commandValue === "string") {
|
||||
command = commandValue;
|
||||
}
|
||||
|
||||
if (Array.isArray(argsValue)) {
|
||||
args = argsValue.map((part) => String(part));
|
||||
}
|
||||
|
||||
const envObject =
|
||||
value.env && typeof value.env === "object" ? (value.env as Record<string, unknown>) : {};
|
||||
const env = Object.entries(envObject).map(([envName, envValue]) => ({
|
||||
name: envName,
|
||||
value: String(envValue),
|
||||
}));
|
||||
|
||||
return {
|
||||
name,
|
||||
command,
|
||||
args,
|
||||
env,
|
||||
};
|
||||
}
|
||||
|
||||
if (type === "remote") {
|
||||
const headersObject =
|
||||
value.headers && typeof value.headers === "object"
|
||||
? (value.headers as Record<string, unknown>)
|
||||
: {};
|
||||
const headers = Object.entries(headersObject).map(([headerName, headerValue]) => ({
|
||||
name: headerName,
|
||||
value: String(headerValue),
|
||||
}));
|
||||
|
||||
return {
|
||||
type: "http" as const,
|
||||
name,
|
||||
url: String(value.url ?? ""),
|
||||
headers,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((entry): entry is NonNullable<typeof entry> => entry !== null);
|
||||
};
|
||||
|
||||
const selectPermissionOption = (
|
||||
options: PermissionOption[],
|
||||
reply: PermissionReplyRequest["reply"],
|
||||
): string | null => {
|
||||
const pick = (...kinds: PermissionOption["kind"][]) => {
|
||||
return options.find((option) => kinds.includes(option.kind))?.optionId ?? null;
|
||||
};
|
||||
|
||||
if (reply === "always") {
|
||||
return pick("allow_always", "allow_once");
|
||||
}
|
||||
|
||||
if (reply === "once") {
|
||||
return pick("allow_once", "allow_always");
|
||||
}
|
||||
|
||||
return pick("reject_once", "reject_always");
|
||||
};
|
||||
|
||||
const waitForSignalOrEvent = async (
|
||||
signal: AbortSignal | undefined,
|
||||
createWaitPromise: () => Promise<void>,
|
||||
) => {
|
||||
if (signal?.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
let done = false;
|
||||
const finish = () => {
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
done = true;
|
||||
if (signal) {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
|
||||
const onAbort = () => finish();
|
||||
|
||||
if (signal) {
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
}
|
||||
|
||||
createWaitPromise().then(finish).catch(finish);
|
||||
});
|
||||
};
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
import type { AgentCapabilities } from "sandbox-agent";
|
||||
|
||||
export type FeatureCoverageView = AgentCapabilities & {
|
||||
export type FeatureCoverageView = {
|
||||
unstable_methods?: boolean;
|
||||
planMode?: boolean;
|
||||
permissions?: boolean;
|
||||
questions?: boolean;
|
||||
toolCalls?: boolean;
|
||||
toolResults?: boolean;
|
||||
textMessages?: boolean;
|
||||
images?: boolean;
|
||||
|
|
@ -15,9 +18,11 @@ export type FeatureCoverageView = AgentCapabilities & {
|
|||
streamingDeltas?: boolean;
|
||||
itemStarted?: boolean;
|
||||
variants?: boolean;
|
||||
sharedProcess?: boolean;
|
||||
};
|
||||
|
||||
export const emptyFeatureCoverage: FeatureCoverageView = {
|
||||
unstable_methods: false,
|
||||
planMode: false,
|
||||
permissions: false,
|
||||
questions: false,
|
||||
|
|
|
|||
145
frontend/packages/inspector/src/types/legacyApi.ts
Normal file
145
frontend/packages/inspector/src/types/legacyApi.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
export type SkillSourceType = "github" | "local" | "git";
|
||||
|
||||
export type SkillSource = {
|
||||
type: SkillSourceType;
|
||||
source: string;
|
||||
skills?: string[];
|
||||
ref?: string;
|
||||
subpath?: string;
|
||||
};
|
||||
|
||||
export type CreateSessionRequest = {
|
||||
agent: string;
|
||||
agentMode?: string;
|
||||
permissionMode?: string;
|
||||
model?: string;
|
||||
variant?: string;
|
||||
mcp?: Record<string, unknown>;
|
||||
skills?: {
|
||||
sources: SkillSource[];
|
||||
};
|
||||
};
|
||||
|
||||
export type AgentModeInfo = {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type AgentModelInfo = {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
variants?: string[];
|
||||
};
|
||||
|
||||
export type AgentInfo = {
|
||||
id: string;
|
||||
installed: boolean;
|
||||
credentialsAvailable: boolean;
|
||||
version?: string | null;
|
||||
path?: string | null;
|
||||
capabilities: Record<string, boolean | undefined>;
|
||||
native_required?: boolean;
|
||||
native_installed?: boolean;
|
||||
native_version?: string | null;
|
||||
agent_process_installed?: boolean;
|
||||
agent_process_source?: string | null;
|
||||
agent_process_version?: string | null;
|
||||
};
|
||||
|
||||
export type ContentPart = {
|
||||
type?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type UniversalItem = {
|
||||
item_id: string;
|
||||
native_item_id?: string | null;
|
||||
parent_id?: string | null;
|
||||
kind: string;
|
||||
role?: string | null;
|
||||
content?: ContentPart[];
|
||||
status?: string | null;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type UniversalEvent = {
|
||||
event_id: string;
|
||||
sequence: number;
|
||||
type: string;
|
||||
source: string;
|
||||
time: string;
|
||||
synthetic?: boolean;
|
||||
data: unknown;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type PermissionEventData = {
|
||||
permission_id: string;
|
||||
status: "requested" | "resolved";
|
||||
action: string;
|
||||
metadata?: unknown;
|
||||
};
|
||||
|
||||
export type QuestionEventData = {
|
||||
question_id: string;
|
||||
status: "requested" | "resolved";
|
||||
prompt: string;
|
||||
options: string[];
|
||||
};
|
||||
|
||||
export type SessionInfo = {
|
||||
sessionId: string;
|
||||
agent: string;
|
||||
eventCount: number;
|
||||
ended?: boolean;
|
||||
model?: string | null;
|
||||
variant?: string | null;
|
||||
permissionMode?: string | null;
|
||||
mcp?: Record<string, unknown>;
|
||||
skills?: {
|
||||
sources?: SkillSource[];
|
||||
};
|
||||
title?: string | null;
|
||||
updatedAt?: string | null;
|
||||
};
|
||||
|
||||
export type EventsQuery = {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
includeRaw?: boolean;
|
||||
};
|
||||
|
||||
export type EventsResponse = {
|
||||
events: UniversalEvent[];
|
||||
};
|
||||
|
||||
export type SessionListResponse = {
|
||||
sessions: SessionInfo[];
|
||||
};
|
||||
|
||||
export type AgentModesResponse = {
|
||||
modes: AgentModeInfo[];
|
||||
};
|
||||
|
||||
export type AgentModelsResponse = {
|
||||
models: AgentModelInfo[];
|
||||
defaultModel?: string | null;
|
||||
};
|
||||
|
||||
export type MessageRequest = {
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type TurnStreamQuery = {
|
||||
includeRaw?: boolean;
|
||||
};
|
||||
|
||||
export type PermissionReplyRequest = {
|
||||
reply: "once" | "always" | "reject";
|
||||
};
|
||||
|
||||
export type QuestionReplyRequest = {
|
||||
answers: string[][];
|
||||
};
|
||||
|
|
@ -2,8 +2,10 @@ export type RequestLog = {
|
|||
id: number;
|
||||
method: string;
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
status?: number;
|
||||
responseBody?: string;
|
||||
time: string;
|
||||
curl: string;
|
||||
error?: string;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ export default defineConfig(({ command }) => ({
|
|||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
"/v2": {
|
||||
target: "http://localhost:2468",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/v1": {
|
||||
target: "http://localhost:2468",
|
||||
changeOrigin: true,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const faqs = [
|
|||
{
|
||||
question: 'How is session data persisted?',
|
||||
answer:
|
||||
"This SDK does not handle persisting session data. Events stream in a universal JSON schema that you can persist anywhere. Consider using Postgres or <a href='https://rivet.gg' target='_blank' rel='noopener noreferrer' class='text-orange-400 hover:underline'>Rivet Actors</a> for data persistence.",
|
||||
"This SDK does not handle persisting session data. In v2, traffic is ACP JSON-RPC over <code>/v2/rpc</code>; persist envelopes in your own storage if you need replay or auditing.",
|
||||
},
|
||||
{
|
||||
question: 'Can I run this locally or does it require a sandbox provider?',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue