Add Bifrost intake Codex plugin
This commit is contained in:
parent
f5c9b1ef59
commit
9258a218ac
6 changed files with 473 additions and 1 deletions
20
.agents/plugins/marketplace.json
Normal file
20
.agents/plugins/marketplace.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "gamecult-local",
|
||||||
|
"interface": {
|
||||||
|
"displayName": "GameCult Local"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "bifrost-intake",
|
||||||
|
"source": {
|
||||||
|
"source": "local",
|
||||||
|
"path": "./plugins/bifrost-intake"
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"installation": "AVAILABLE",
|
||||||
|
"authentication": "ON_INSTALL"
|
||||||
|
},
|
||||||
|
"category": "Coding"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -58,9 +58,24 @@ node .\tools\agent-transport.mjs apply-snapshot --in E:\tmp\bifrost-agent-transp
|
||||||
|
|
||||||
The `snapshot` command writes a MessagePack-encoded `cultnet.snapshot_response_raw.v0` message. This keeps CultCache payload bytes intact for peers that share the document binding.
|
The `snapshot` command writes a MessagePack-encoded `cultnet.snapshot_response_raw.v0` message. This keeps CultCache payload bytes intact for peers that share the document binding.
|
||||||
|
|
||||||
|
## Codex Intake Plugin
|
||||||
|
|
||||||
|
`plugins/bifrost-intake` exposes this lane to Codex as an MCP plugin. It does not own a second queue. Its server wraps `tools/agent-transport.mjs`.
|
||||||
|
|
||||||
|
Tools:
|
||||||
|
|
||||||
|
- `enqueue_update_request`
|
||||||
|
- `list_update_requests`
|
||||||
|
- `claim_update_request`
|
||||||
|
- `close_update_request`
|
||||||
|
- `format_claimed_request`
|
||||||
|
- `create_transport_snapshot`
|
||||||
|
- `apply_transport_snapshot`
|
||||||
|
|
||||||
|
The plugin is listed in `.agents/plugins/marketplace.json` as `bifrost-intake`.
|
||||||
|
|
||||||
## Next Integration Points
|
## Next Integration Points
|
||||||
|
|
||||||
- Add an MCP wrapper that calls this CLI instead of storing transport state in VoidBot.
|
|
||||||
- Teach the VoidBot consensus feeder to enqueue Bifrost update requests after it writes a packet.
|
- Teach the VoidBot consensus feeder to enqueue Bifrost update requests after it writes a packet.
|
||||||
- Let repo Faces claim by `targetRepoName`, then inject the claimed packet into Codex only when the repo matches their jurisdiction.
|
- Let repo Faces claim by `targetRepoName`, then inject the claimed packet into Codex only when the repo matches their jurisdiction.
|
||||||
- Use CultNet direct pipes when a long-running agent runtime is available; keep raw snapshot files as the dead-simple bridge until then.
|
- Use CultNet direct pipes when a long-running agent runtime is available; keep raw snapshot files as the dead-simple bridge until then.
|
||||||
|
|
|
||||||
43
plugins/bifrost-intake/.codex-plugin/plugin.json
Normal file
43
plugins/bifrost-intake/.codex-plugin/plugin.json
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"name": "bifrost-intake",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Codex intake tools for Bifrost agent update requests stored in CultCache and synced through CultNet.",
|
||||||
|
"author": {
|
||||||
|
"name": "GameCult",
|
||||||
|
"email": "ops@gamecult.org",
|
||||||
|
"url": "https://github.com/GameCult"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/GameCult/Bifrost",
|
||||||
|
"repository": "https://github.com/GameCult/Bifrost",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"bifrost",
|
||||||
|
"agent-transport",
|
||||||
|
"cultcache",
|
||||||
|
"cultnet",
|
||||||
|
"codex"
|
||||||
|
],
|
||||||
|
"skills": "./skills/",
|
||||||
|
"mcpServers": "./.mcp.json",
|
||||||
|
"interface": {
|
||||||
|
"displayName": "Bifrost Intake",
|
||||||
|
"shortDescription": "Claim and route Bifrost agent update requests",
|
||||||
|
"longDescription": "Expose Bifrost's CultCache-backed, CultNet-syncable agent transport lane to Codex so repo agents can enqueue, list, claim, close, and package update requests.",
|
||||||
|
"developerName": "GameCult",
|
||||||
|
"category": "Coding",
|
||||||
|
"capabilities": [
|
||||||
|
"Interactive",
|
||||||
|
"Write"
|
||||||
|
],
|
||||||
|
"websiteURL": "https://github.com/GameCult/Bifrost",
|
||||||
|
"privacyPolicyURL": "https://github.com/GameCult/Bifrost",
|
||||||
|
"termsOfServiceURL": "https://github.com/GameCult/Bifrost",
|
||||||
|
"defaultPrompt": [
|
||||||
|
"Claim the next Bifrost update request for this repo.",
|
||||||
|
"Enqueue this consensus packet for a repo Face.",
|
||||||
|
"List queued Bifrost agent requests."
|
||||||
|
],
|
||||||
|
"brandColor": "#4F46E5",
|
||||||
|
"screenshots": []
|
||||||
|
}
|
||||||
|
}
|
||||||
9
plugins/bifrost-intake/.mcp.json
Normal file
9
plugins/bifrost-intake/.mcp.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"bifrost-intake": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["./scripts/mcp-server.mjs"],
|
||||||
|
"note": "Local Bifrost intake MCP server. Wraps Bifrost's CultCache/CultNet agent transport CLI."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
366
plugins/bifrost-intake/scripts/mcp-server.mjs
Normal file
366
plugins/bifrost-intake/scripts/mcp-server.mjs
Normal file
|
|
@ -0,0 +1,366 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { dirname, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const pluginRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const bifrostRoot = resolve(pluginRoot, "..", "..");
|
||||||
|
const transportCli = resolve(bifrostRoot, "tools", "agent-transport.mjs");
|
||||||
|
const serverInfo = { name: "bifrost-intake", version: "0.1.0" };
|
||||||
|
|
||||||
|
const tools = [
|
||||||
|
{
|
||||||
|
name: "enqueue_update_request",
|
||||||
|
description: "Enqueue a Bifrost agent update request in the CultCache-backed intake store.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ["repo", "title", "requestMarkdown"],
|
||||||
|
properties: {
|
||||||
|
repo: { type: "string", description: "Target repository short name, such as AetheriaLore." },
|
||||||
|
agent: { type: "string", description: "Optional target Face identity, such as nibu." },
|
||||||
|
title: { type: "string" },
|
||||||
|
requestMarkdown: { type: "string", description: "Consensus/task packet to inject when claimed." },
|
||||||
|
priority: { type: "integer", default: 50 },
|
||||||
|
repoFullName: { type: "string" },
|
||||||
|
sourceKind: { type: "string", default: "codex" },
|
||||||
|
sourceChannelId: { type: "string" },
|
||||||
|
sourceMessageIds: { type: "array", items: { type: "string" } },
|
||||||
|
sourcePacketPath: { type: "string" },
|
||||||
|
sourcePromptPath: { type: "string" },
|
||||||
|
createdBy: { type: "string" },
|
||||||
|
store: { type: "string", description: "Optional override path for the .cc store." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list_update_requests",
|
||||||
|
description: "List Bifrost agent update requests, optionally filtered by repo, Face, or status.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
properties: {
|
||||||
|
repo: { type: "string" },
|
||||||
|
agent: { type: "string" },
|
||||||
|
status: { type: "string", enum: ["queued", "claimed", "completed", "cancelled"] },
|
||||||
|
store: { type: "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "claim_update_request",
|
||||||
|
description: "Claim the highest-priority queued request for a repository jurisdiction.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ["repo"],
|
||||||
|
properties: {
|
||||||
|
repo: { type: "string" },
|
||||||
|
agent: { type: "string" },
|
||||||
|
claimedBy: { type: "string" },
|
||||||
|
store: { type: "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "close_update_request",
|
||||||
|
description: "Mark a Bifrost update request completed or cancelled.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ["id", "status"],
|
||||||
|
properties: {
|
||||||
|
id: { type: "string" },
|
||||||
|
status: { type: "string", enum: ["completed", "cancelled"] },
|
||||||
|
note: { type: "string" },
|
||||||
|
store: { type: "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "format_claimed_request",
|
||||||
|
description: "Return a Codex-ready prompt packet for a claimed update request id.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ["id"],
|
||||||
|
properties: {
|
||||||
|
id: { type: "string" },
|
||||||
|
store: { type: "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create_transport_snapshot",
|
||||||
|
description: "Write a CultNet raw snapshot of the Bifrost intake store.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ["out"],
|
||||||
|
properties: {
|
||||||
|
out: { type: "string" },
|
||||||
|
messageId: { type: "string" },
|
||||||
|
store: { type: "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "apply_transport_snapshot",
|
||||||
|
description: "Apply a CultNet raw snapshot into the Bifrost intake store.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ["in"],
|
||||||
|
properties: {
|
||||||
|
in: { type: "string" },
|
||||||
|
store: { type: "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let inputBuffer = Buffer.alloc(0);
|
||||||
|
|
||||||
|
process.stdin.on("data", (chunk) => {
|
||||||
|
inputBuffer = Buffer.concat([inputBuffer, chunk]);
|
||||||
|
drainMessages().catch((error) => {
|
||||||
|
sendError(null, -32603, error.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function drainMessages() {
|
||||||
|
while (true) {
|
||||||
|
const headerEnd = inputBuffer.indexOf("\r\n\r\n");
|
||||||
|
if (headerEnd === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const header = inputBuffer.subarray(0, headerEnd).toString("utf8");
|
||||||
|
const lengthMatch = /^Content-Length:\s*(\d+)/imu.exec(header);
|
||||||
|
if (!lengthMatch) {
|
||||||
|
throw new Error("MCP message is missing Content-Length header.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const length = Number(lengthMatch[1]);
|
||||||
|
const bodyStart = headerEnd + 4;
|
||||||
|
const bodyEnd = bodyStart + length;
|
||||||
|
if (inputBuffer.length < bodyEnd) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawBody = inputBuffer.subarray(bodyStart, bodyEnd).toString("utf8");
|
||||||
|
inputBuffer = inputBuffer.subarray(bodyEnd);
|
||||||
|
await handleMessage(JSON.parse(rawBody));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMessage(message) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(message, "id")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (message.method) {
|
||||||
|
case "initialize":
|
||||||
|
sendResult(message.id, {
|
||||||
|
protocolVersion: message.params?.protocolVersion ?? "2024-11-05",
|
||||||
|
capabilities: { tools: {} },
|
||||||
|
serverInfo,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
case "tools/list":
|
||||||
|
sendResult(message.id, { tools });
|
||||||
|
return;
|
||||||
|
case "tools/call":
|
||||||
|
sendResult(message.id, await callTool(message.params ?? {}));
|
||||||
|
return;
|
||||||
|
case "ping":
|
||||||
|
sendResult(message.id, {});
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
sendError(message.id, -32601, `Unknown method "${message.method}".`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
sendError(message.id, -32603, error instanceof Error ? error.message : String(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callTool(params) {
|
||||||
|
const name = params.name;
|
||||||
|
const args = params.arguments ?? {};
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
|
case "enqueue_update_request":
|
||||||
|
return jsonToolResult(await runTransport([
|
||||||
|
"enqueue",
|
||||||
|
"--repo", requireString(args.repo, "repo"),
|
||||||
|
...optionalArg("--agent", args.agent),
|
||||||
|
"--title", requireString(args.title, "title"),
|
||||||
|
"--request", requireString(args.requestMarkdown, "requestMarkdown"),
|
||||||
|
"--priority", String(args.priority ?? 50),
|
||||||
|
...optionalArg("--repo-full-name", args.repoFullName),
|
||||||
|
...optionalArg("--source-kind", args.sourceKind ?? "codex"),
|
||||||
|
...optionalArg("--source-channel-id", args.sourceChannelId),
|
||||||
|
...optionalArg("--source-message-ids", Array.isArray(args.sourceMessageIds) ? args.sourceMessageIds.join(",") : undefined),
|
||||||
|
...optionalArg("--packet-path", args.sourcePacketPath),
|
||||||
|
...optionalArg("--prompt-path", args.sourcePromptPath),
|
||||||
|
...optionalArg("--created-by", args.createdBy),
|
||||||
|
...storeArg(args.store),
|
||||||
|
]));
|
||||||
|
case "list_update_requests":
|
||||||
|
return jsonToolResult(await runTransport([
|
||||||
|
"list",
|
||||||
|
...optionalArg("--repo", args.repo),
|
||||||
|
...optionalArg("--agent", args.agent),
|
||||||
|
...optionalArg("--status", args.status),
|
||||||
|
...storeArg(args.store),
|
||||||
|
]));
|
||||||
|
case "claim_update_request":
|
||||||
|
return jsonToolResult(await runTransport([
|
||||||
|
"claim",
|
||||||
|
"--repo", requireString(args.repo, "repo"),
|
||||||
|
...optionalArg("--agent", args.agent),
|
||||||
|
...optionalArg("--claimed-by", args.claimedBy),
|
||||||
|
...storeArg(args.store),
|
||||||
|
]));
|
||||||
|
case "close_update_request":
|
||||||
|
return jsonToolResult(await runTransport([
|
||||||
|
"close",
|
||||||
|
"--id", requireString(args.id, "id"),
|
||||||
|
"--status", requireString(args.status, "status"),
|
||||||
|
...optionalArg("--note", args.note),
|
||||||
|
...storeArg(args.store),
|
||||||
|
]));
|
||||||
|
case "format_claimed_request":
|
||||||
|
return textToolResult(formatClaimedRequest(await requireRequestById(args)));
|
||||||
|
case "create_transport_snapshot":
|
||||||
|
return jsonToolResult(await runTransport([
|
||||||
|
"snapshot",
|
||||||
|
"--out", requireString(args.out, "out"),
|
||||||
|
...optionalArg("--message-id", args.messageId),
|
||||||
|
...storeArg(args.store),
|
||||||
|
]));
|
||||||
|
case "apply_transport_snapshot":
|
||||||
|
return jsonToolResult(await runTransport([
|
||||||
|
"apply-snapshot",
|
||||||
|
"--in", requireString(args.in, "in"),
|
||||||
|
...storeArg(args.store),
|
||||||
|
]));
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown tool "${name}".`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requireRequestById(args) {
|
||||||
|
const id = requireString(args.id, "id");
|
||||||
|
const requests = await runTransport(["list", ...storeArg(args.store)]);
|
||||||
|
const request = requests.find((candidate) => candidate.id === id);
|
||||||
|
if (!request) {
|
||||||
|
throw new Error(`No Bifrost update request found for id "${id}".`);
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatClaimedRequest(request) {
|
||||||
|
return `# Bifrost Update Request
|
||||||
|
|
||||||
|
ID: ${request.id}
|
||||||
|
Status: ${request.status}
|
||||||
|
Target repo: ${request.targetRepoName}
|
||||||
|
Target Face: ${request.targetAgentIdentity ?? "any"}
|
||||||
|
Priority: ${request.priority}
|
||||||
|
Title: ${request.title}
|
||||||
|
|
||||||
|
## Request
|
||||||
|
|
||||||
|
${request.requestMarkdown}
|
||||||
|
|
||||||
|
## Handling
|
||||||
|
|
||||||
|
Work only if this request matches the current repository jurisdiction. When finished, close the request through Bifrost intake and include the PR, commit, or cancellation reason in the close note.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTransport(args) {
|
||||||
|
return new Promise((resolvePromise, rejectPromise) => {
|
||||||
|
const child = spawn(process.execPath, [transportCli, ...args], {
|
||||||
|
cwd: bifrostRoot,
|
||||||
|
windowsHide: true,
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
child.stdout.on("data", (chunk) => {
|
||||||
|
stdout += chunk.toString("utf8");
|
||||||
|
});
|
||||||
|
child.stderr.on("data", (chunk) => {
|
||||||
|
stderr += chunk.toString("utf8");
|
||||||
|
});
|
||||||
|
child.on("error", rejectPromise);
|
||||||
|
child.on("close", (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
rejectPromise(new Error(stderr.trim() || `agent-transport exited with code ${code}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
resolvePromise(JSON.parse(stdout));
|
||||||
|
} catch (error) {
|
||||||
|
rejectPromise(new Error(`agent-transport returned non-JSON output: ${stdout || stderr}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonToolResult(value) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(value, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function textToolResult(text) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionalArg(flag, value) {
|
||||||
|
if (typeof value !== "string" || value.trim().length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [flag, value.trim()];
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeArg(value) {
|
||||||
|
return optionalArg("--store", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireString(value, name) {
|
||||||
|
if (typeof value !== "string" || value.trim().length === 0) {
|
||||||
|
throw new Error(`Missing required argument "${name}".`);
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendResult(id, result) {
|
||||||
|
sendMessage({ jsonrpc: "2.0", id, result });
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendError(id, code, message) {
|
||||||
|
sendMessage({ jsonrpc: "2.0", id, error: { code, message } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMessage(message) {
|
||||||
|
const body = Buffer.from(JSON.stringify(message), "utf8");
|
||||||
|
process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
|
||||||
|
process.stdout.write(body);
|
||||||
|
}
|
||||||
19
plugins/bifrost-intake/skills/bifrost-intake/SKILL.md
Normal file
19
plugins/bifrost-intake/skills/bifrost-intake/SKILL.md
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
---
|
||||||
|
name: bifrost-intake
|
||||||
|
description: Use when Codex should enqueue, list, claim, close, or package Bifrost agent update requests for repo Faces through the Bifrost intake MCP tools.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bifrost Intake
|
||||||
|
|
||||||
|
Bifrost owns agent update request semantics. CultCache stores the request documents. CultNet moves raw snapshots and document updates. This plugin exposes that lane to Codex through MCP tools.
|
||||||
|
|
||||||
|
Use these tools when a user asks to feed a consensus packet to a repo Face, claim work for the current repo, inspect queued update requests, or close a request after work is handled.
|
||||||
|
|
||||||
|
Default flow:
|
||||||
|
|
||||||
|
1. Use `list_update_requests` to inspect queued work for a repo or Face.
|
||||||
|
2. Use `claim_update_request` with the repo name before injecting the request into Codex.
|
||||||
|
3. Use `format_claimed_request` when you need a compact prompt packet for a claimed request.
|
||||||
|
4. Use `close_update_request` when the work is completed or cancelled.
|
||||||
|
|
||||||
|
Do not store a parallel queue in VoidBot or Codex session memory. If the request is intended to persist, put it in Bifrost intake.
|
||||||
Loading…
Reference in a new issue