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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ Agents API is the primary infrastructure deliverable. Parsar is an ordinary clie

### Optional application example

`example/parsar/` is an optional, independently started Agent workbench. Its [README](example/parsar/README.md) owns its product behavior. The boundary rules are:
`example/parsar/` is an optional, independently started Agent workbench maintained in this repository, with no dependency on the external Parsar product repository. Its [README](example/parsar/README.md) owns its product behavior. The boundary rules are:

- It calls only public `/v1` APIs. Its Project key stays server-side; it never holds a Core key or issues machine credentials. Core-only credential issuance stays in the operator console.
- It may reuse product UI and keep a small product-owned SQLite database (Node's built-in module, Node 22.13+), outside the checkout and isolated by Core origin and Project key fingerprint. Provider keys never reach the browser.
- It calls only public `/v1` APIs. Its Project key stays server-side; it never holds a Core key or issues machine credentials. Self-hosted connection displays the public Session installation command unchanged; Core owns bootstrap authorization and machine credential issuance.
- It may keep a small product-owned SQLite database (Node's built-in module, Node 22.13+), outside the checkout and isolated by Core origin and Project key fingerprint. Provider keys never reach the browser.
- Core owns Skills and all execution and history state. The example stores only Session references and pending creation requests with stable idempotency keys.
- Product resources use `/app/` and never become Core API or database conventions.
- It is excluded from Core distributions and cannot become a service dependency.
Expand Down
59 changes: 30 additions & 29 deletions example/parsar/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Parsar Agent workbench

A small product application on OpenAgentCore, using Parsar UI. Manage models,
An independently started small application on OpenAgentCore. Manage models,
Skills, HTTP MCP services and runtime configurations; compose reusable Agent
configurations and start independent Sessions. Continue the same Session to keep
its history and live workspace. There is no task layer or shared workspace.
Expand Down Expand Up @@ -33,9 +33,12 @@ For a built version, run `pnpm --filter @oac/parsar-example build` and then
to the browser; leaving the key blank while editing preserves it. Discovery
sends only that Provider's key and does not follow redirects. The list expects
the OpenAI-shaped `data: [{id: "..."}]` response. Discovery failures leave manual
entry available. This imports the catalog only: Core still owns execution
connections for hosted/text-only Sessions. Self-hosted Sessions instead pass
this Provider URL/key explicitly to Core, using the selected harness protocol.
entry available. Codex and Claude Code workspace Sessions pass the selected Provider URL/key
explicitly to Core, using the harness protocol. These Sessions require an HTTPS
Base URL and API key. Text-only Sessions and hosted MiniMax Code use Core's
deployment connection; the creation dialog states that their Provider is only
a catalog group. The example does not yet expose MiniMax Code's required token
limits.
2. **Skills:** create a SKILL.md resource or upload a ZIP. Inspect versions,
upload a new version, and choose the default. Core validates and stores bundles.
3. **MCP:** save anonymous HTTPS endpoints and bind them to Agents. Hosted
Expand All @@ -53,7 +56,18 @@ For a built version, run `pnpm --filter @oac/parsar-example build` and then
Create, copy and edit configurations without allocating a runtime.
6. **Sessions:** open an Agent, choose a runtime and send the first message.
Read streaming replies and tool activity, continue the conversation, cancel a running turn and reopen
history. Each hosted Session gets its own workspace; text-only has no workspace.
history. Unsupported Skill/MCP/runtime combinations are explained before creation.
The Session's **Files** dialog uploads one file at a time (up to 5 MiB) through
Core's public Environment Files API while idle. Each upload gets a unique path
under `/workspace/inputs`; a path relative to the working directory is inserted into the message draft,
not sent automatically. Unsent drafts, including uploaded file paths, survive
reloads and navigation in the same browser tab and remain scoped to the Session. This keeps the API's `/workspace` alias distinct from
the physical user-machine directory. Ask the Agent to save generated files
under `./outputs`;
published Artifacts can be paged and downloaded from the same dialog, even when
the live workspace is unavailable. Downloads remain binary; errors stay in the
dialog. Text-only Sessions have no file actions.
Each hosted Session gets its own workspace; text-only has no workspace.

Agent edits and catalog updates apply to future Sessions. Core freezes existing
Session configuration, including resolved Skill versions. Continuing a Session
Expand All @@ -66,12 +80,12 @@ model availability or MCP connectivity. Skills require a hosted runtime.
The loopback Node server has two small responsibilities: a fixed allowlist proxy
for public Core resources, and `/app/` CRUD for product-owned configuration. One
SQLite table stores typed JSON records. There is no ORM, background scheduler,
execution database or imported Parsar backend.
execution database.

SQLite files live in `~/.oac/data/parsar-example/`. Set the absolute
`OAC_EXAMPLE_DATA_DIR` to relocate them. A hash of the Core origin and Project key
selects the file; changing either selects a different local catalog. Back up the
SQLite file with the server stopped. No key is stored in it. Builds and caches
SQLite file with the server stopped. The Core Project key is not stored in it. Provider keys are stored there. Builds and caches
use `${OAC_DEV_HOME:-$HOME/.oac}`.

Use one server process and a dedicated Project for this local single-user example.
Expand All @@ -97,7 +111,8 @@ A few more implementation details:
- **Providers and models** are saved together in one SQLite transaction. Editing
reads a Provider and its models as one snapshot; changing a model advances the
Provider's revision.
- **Browser calls** use `OpenAIAgentsClient`; only Session creation goes through a
- **Browser calls** use `OpenAIAgentsClient`, with a small public HTTP reader for
Artifacts (not yet exposed by that client); only Session creation goes through a
small server adapter. Closing the browser aborts the upstream stream, never the
running Session.
- **Workspaces:** hosted Sessions each get their own. User-machine Sessions use the
Expand Down Expand Up @@ -136,28 +151,16 @@ checks Skill/Plugin installation, same-Session file reuse, and workspace isolati
between two Sessions. It asks the model to call DeepWiki; inspect the recorded
tool activity to distinguish a successful call from an attempted call.

## UI provenance

The copied UI primitives, `src/lib/utils.ts`, `src/style.css`
and `public/*` originate in [Parsar](https://github.com/MiniMax-AI-Dev/parsar)
revision `90fafede`, under [MIT](LICENSE). The responsive body minimum width and
application pages are local adaptations. Navigation animation adapts
[Motion Primitives](https://github.com/ibelick/motion-primitives), with reduced-motion
support and its [MIT notice](MOTION-PRIMITIVES-LICENSE). Multica's resource and
Agent organization informs the product flow; no Multica code is copied.

## Connect a user machine

Run a matching Core and daemon version.

Create a user-machine runtime and start a Session with an Agent using Codex or
Claude Code. No initial Turn is sent. Open **Connect user machine**, obtain the
Session's executor credential in Core Web (Session log), save the credential on
the host as instructed, and run the generated platform-specific install/start
command. The daemon binary and native harness must already be installed. Windows
Claude Code also requires Git Bash. The page reads Core's public Environment
Claude Code. No initial Turn is sent. Open **Connect user machine** and run the
Core-provided command on the target host. The installer downloads the matching
native distribution, installs the selected harness and starts the connection.
Windows Claude Code also requires Git Bash. The page reads Core's public Environment
status and enables sending after it reports connected. The example backend does
not need or accept the administrator Core key.
not need or accept the administrator Core key. Commands carry temporary
Environment-scoped authorization; do not share them. Session refresh renews them.

The selected model Provider needs an HTTPS Base URL and API key. Its protocol is
Responses for Codex and Anthropic for Claude Code. Core freezes and delivers the
Expand All @@ -172,6 +175,4 @@ included in this example because its required token limits are not exposed.

The daemon runs with the starting user's permissions and adds no sandbox. Runtime
home is separate per Session; stopping it preserves local files. Credential
rotation and revocation remain Core console operations. Native install commands
are for fresh installations; restart an existing one with the same Runtime home
and `start`, without rerunning `install`.
rotation and revocation remain Core console operations. Reuse the original installation directory when reconnecting.
28 changes: 26 additions & 2 deletions example/parsar/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import { productAPI } from "./server/product.mjs";
const root = fileURLToPath(new URL(".", import.meta.url));
const routes = [
[/^\/v1\/agents\/environments\/[a-f0-9-]{36}$/, ["GET"]],
[/^\/v1\/agents\/environments\/[a-f0-9-]{36}\/files$/, ["GET", "POST"]],
[/^\/v1\/agents\/sessions\/[a-f0-9-]{36}\/artifacts$/, ["GET"]],
[
/^\/v1\/agents\/sessions\/[a-f0-9-]{36}\/artifacts\/[a-f0-9-]{36}\/content$/,
["GET"],
],
[/^\/v1\/agents\/sessions\/[a-f0-9-]{36}$/, ["GET"]],
[/^\/v1\/agents\/sessions\/[a-f0-9-]{36}\/(items|turns)$/, ["GET"]],
[/^\/v1\/agents\/sessions\/[a-f0-9-]{36}\/events$/, ["GET", "POST"]],
Expand Down Expand Up @@ -189,7 +195,12 @@ export function createHandler(
const streaming =
req.method === "GET" && url.pathname.endsWith("/events");
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 30_000);
const fileWrite =
req.method === "POST" && url.pathname.endsWith("/files");
const timer = setTimeout(
() => controller.abort(),
fileWrite ? 240_000 : 30_000,
);
res.on("close", () => controller.abort());
try {
const chunks = [];
Expand All @@ -198,7 +209,8 @@ export function createHandler(
size += chunk.length;
if (
size >
(url.pathname.startsWith("/v1/skills")
(url.pathname.startsWith("/v1/skills") ||
url.pathname.endsWith("/files")
? 8 * 1024 * 1024
: 1024 * 1024)
)
Expand Down Expand Up @@ -249,6 +261,18 @@ export function createHandler(
await pipeline(Readable.fromWeb(upstream.body), res);
return;
}
if (url.pathname.endsWith("/content") && upstream.ok && upstream.body) {
// Downloads stay binary and are never rendered as active browser content.
clearTimeout(timer);
res.writeHead(upstream.status, {
"Content-Type": "application/octet-stream",
"Content-Disposition":
upstream.headers.get("content-disposition") || "attachment",
"Cache-Control": "no-store",
});
await pipeline(Readable.fromWeb(upstream.body), res);
return;
}
const body = await upstream.text();
res.writeHead(upstream.status, {
"Content-Type": "application/json",
Expand Down
31 changes: 7 additions & 24 deletions example/parsar/server/sessions.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { sessionRestriction } from "../shared/session-profile.mjs";
import { zipSync, strToU8 } from "fflate";
import { AppError, text, uuid } from "./store.mjs";

Expand All @@ -14,41 +15,28 @@ function requestFor(store, id, body) {
throw new AppError(400, "绑定的 MCP 服务标识不能重复。");
const environment = { type: runtime.environment };
const selfHosted = environment.type === "self_hosted";
const restriction = sessionRestriction(agent, runtime);
if (restriction) throw new AppError(400, restriction);
let modelProvider;
if (selfHosted) {
if (agent.skill_ids.length)
throw new AppError(
400,
"此示例的用户机器只使用本地能力目录,暂不传递托管 Skill。请使用未绑定托管 Skill 的 Agent,并在运行时填写本地能力目录。",
);
if (mcps.length)
throw new AppError(
400,
"用户机器的 MCP 请通过本地 Plugin 能力目录配置。当前 Core 不接受此处的 MCP 服务绑定,请使用未绑定 MCP 的 Agent。",
);
if (agent.harness === "mcode")
throw new AppError(
400,
"此示例的用户机器接入先支持 Codex 和 Claude Code,请选择其中一个执行引擎。",
);
if (environment.type !== "none" && agent.harness !== "mcode") {
const provider = store.get("providers", model.provider_id);
if (!provider?.base_url?.startsWith("https://") || !provider.api_key)
throw new AppError(
400,
"用户机器需要模型 Provider 的 HTTPS Base URL 和 API Key,请先在模型页面配置。",
"请先在模型页面配置所选 Provider 的 HTTPS Base URL 和 API Key。",
);
modelProvider = {
protocol: agent.harness === "codex" ? "responses" : "anthropic",
base_url: provider.base_url,
api_key: provider.api_key,
};
}
if (selfHosted) {
environment.workspace_directory = runtime.workspace_directory;
environment.capability_directories = runtime.capability_directories || [];
}
const tools = [{ type: "web_search", mode: "disabled" }];
if (environment.type === "none" || selfHosted) {
if (agent.skill_ids.length)
throw new AppError(400, "Skills 需要托管运行环境。");
tools.push(
...mcps.map((mcp) => ({
type: "mcp",
Expand All @@ -64,11 +52,6 @@ function requestFor(store, id, body) {
skill_id,
}));
if (mcps.length) {
if (agent.harness === "mcode")
throw new AppError(
400,
"MiniMax Code 暂不支持托管环境中的 HTTP MCP,请选择 Claude Code 或 Codex。",
);
const name = "agent-mcp";
const description = "Agent HTTP MCP bindings";
const archive = zipSync({
Expand Down
4 changes: 4 additions & 0 deletions example/parsar/shared/session-profile.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export function sessionRestriction(
agent: { harness: string; skill_ids: string[]; mcp_ids: string[] },
runtime?: { environment: string },
): string | undefined;
16 changes: 16 additions & 0 deletions example/parsar/shared/session-profile.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Product guidance only; Core remains authoritative for capability validation.
export function sessionRestriction(agent, runtime) {
if (!runtime) return undefined;
if (runtime.environment === "self_hosted") {
if (agent.skill_ids.length)
return "用户机器使用本地能力目录,不能绑定托管 Skill。请先编辑 Agent,移除托管 Skill。";
if (agent.mcp_ids.length)
return "用户机器的 MCP 需通过本地 Plugin 配置。请先编辑 Agent,移除 MCP 绑定。";
if (agent.harness === "mcode")
return "当前示例的用户机器支持 Codex 和 Claude Code,请先编辑 Agent 的执行引擎。";
}
if (runtime.environment === "none" && agent.skill_ids.length)
return "Skills 需要托管运行环境,请选择托管沙箱或移除 Skill 绑定。";
if (agent.harness === "mcode" && agent.mcp_ids.length)
return "MiniMax Code 暂不支持此处的 HTTP MCP,请选择 Claude Code 或 Codex。";
}
24 changes: 23 additions & 1 deletion example/parsar/src/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ export function Composer({
session,
turnId,
connectionPending = false,
attachment = "",
onPendingChange,
}: {
session: AgentSession;
turnId?: string;
connectionPending?: boolean;
attachment?: string;
onPendingChange?: (pending: boolean) => void;
}) {
const cache = useQueryClient();
const storageKey = `oac-example-message-${session.id}`;
Expand All @@ -30,7 +34,24 @@ export function Composer({
}
},
);
const [text, setText] = useState(pending?.text || "");
useEffect(() => {
onPendingChange?.(Boolean(pending));
}, [pending, onPendingChange]);
const draftKey = `oac-example-draft-${session.id}`;
const [text, setText] = useState(
() => pending?.text || sessionStorage.getItem(draftKey) || "",
);
useEffect(() => {
if (text) sessionStorage.setItem(draftKey, text);
else sessionStorage.removeItem(draftKey);
}, [draftKey, text]);
useEffect(() => {
if (attachment)
setText(
(previous) =>
`${previous}${previous ? "\n" : ""}请处理文件:${attachment}`,
);
}, [attachment]);
const [cancelKey, setCancelKey] = useState(createIdempotencyKey);
const [cancelRequested, setCancelRequested] = useState(false);
useEffect(() => {
Expand Down Expand Up @@ -62,6 +83,7 @@ export function Composer({
requestReturned(session.id, operation.key);
},
onSuccess: () => {
sessionStorage.removeItem(draftKey);
setText("");
setPending(null);
sessionStorage.removeItem(storageKey);
Expand Down
2 changes: 1 addition & 1 deletion example/parsar/src/ConfigurationFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export function ConfigurationFields({
<Help>
{group.key === "skill_ids"
? "最多选择 8 个技能,运行时需支持托管沙箱。新会话使用技能的默认版本。"
: "最多选择 8 个 MCP。Core 负责连接服务,服务地址需从 Core 网络可达。"}
: "最多选择 8 个 MCP。托管会话从工作区连接,纯文本会话从 Core 连接;用户机器请在本地 Plugin 中配置。"}
</Help>
</div>
<div className="max-h-48 space-y-2 overflow-auto">
Expand Down
Loading