@rivus/agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,225 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { Effect } from "effect";
3
+ import * as Lark from "@larksuiteoapi/node-sdk";
4
+ import { AuthStorage, createAgentSession, ModelRegistry, SessionManager } from "@earendil-works/pi-coding-agent";
5
+ import {
6
+ createJsonFetchRequest,
7
+ createJsonFileFeishuCardTargetRegistry,
8
+ createJsonlAgentEventLog,
9
+ createLazyFeishuWebSocketEventDispatcher,
10
+ createRivusDaemonStatusHttpServer,
11
+ createPiAgentLoop,
12
+ createPiSessionRegistry,
13
+ createSystemClock,
14
+ createUuidRunIds,
15
+ restoreConfiguredRivusDaemonBootstrap,
16
+ type ConfiguredRivusDaemonBootstrapRequest,
17
+ type ConfiguredRivusDaemonBootstrapResponse,
18
+ type FeishuWebSocketClient,
19
+ type RivusDaemonBootstrapContext,
20
+ type RivusDaemonFeishuReplayRunner,
21
+ type RivusDaemonProcess,
22
+ type RivusDaemonPromptRunner,
23
+ type RivusDaemonStatusReporter,
24
+ type FeishuReceiveMessagePayload,
25
+ type FeishuReceiveMessageReplayOptions
26
+ } from "@rivus/agent";
27
+
28
+ type PiSessionOptions = NonNullable<Parameters<typeof createAgentSession>[0]>;
29
+
30
+ const STATE_DIR = "./.rivus";
31
+ const CARD_TARGETS_FILE = `${STATE_DIR}/feishu-card-targets.json`;
32
+ const EVENT_LOG_FILE = `${STATE_DIR}/agent-events.jsonl`;
33
+ const PI_AUTH_FILE = `${STATE_DIR}/pi-auth.json`;
34
+ const PI_MODELS_FILE = `${STATE_DIR}/pi-models.json`;
35
+
36
+ export async function createRivusDaemonProcess(
37
+ context: RivusDaemonBootstrapContext
38
+ ): Promise<RivusDaemonProcess & RivusDaemonPromptRunner & RivusDaemonStatusReporter & RivusDaemonFeishuReplayRunner> {
39
+ await mkdir(STATE_DIR, { recursive: true });
40
+
41
+ const piSessionOptions = await createPiSessionOptions(context);
42
+ const sessionRegistry = createPiSessionRegistry({
43
+ createSession: async () => {
44
+ const result = await createAgentSession({
45
+ ...piSessionOptions,
46
+ sessionManager: SessionManager.create(process.cwd())
47
+ });
48
+ return {
49
+ dispose: () => result.session.dispose(),
50
+ session: result.session
51
+ };
52
+ }
53
+ });
54
+ const piLoop = createPiAgentLoop({
55
+ disposeSessionAfterRun: false,
56
+ resolveSession: (input) => sessionRegistry.resolve(input)
57
+ });
58
+ const statusPort = readOptionalPort(context.env.RIVUS_STATUS_PORT);
59
+ const request = createJsonFetchRequest();
60
+
61
+ const bootstrap = await Effect.runPromise(
62
+ restoreConfiguredRivusDaemonBootstrap({
63
+ cardTargets: createJsonFileFeishuCardTargetRegistry({
64
+ filePath: CARD_TARGETS_FILE
65
+ }),
66
+ clock: createSystemClock(),
67
+ config: context.config,
68
+ ...(statusPort === undefined
69
+ ? {}
70
+ : {
71
+ createStatusTransport: (statusReporter: RivusDaemonStatusReporter) =>
72
+ createRivusDaemonStatusHttpServer({
73
+ port: statusPort,
74
+ statusReporter
75
+ })
76
+ }),
77
+ eventDispatcher: createLazyFeishuWebSocketEventDispatcher(() => new Lark.EventDispatcher({})),
78
+ eventLog: createJsonlAgentEventLog({
79
+ filePath: EVENT_LOG_FILE
80
+ }),
81
+ loop: piLoop,
82
+ request: (input: ConfiguredRivusDaemonBootstrapRequest) =>
83
+ request(input).pipe(Effect.map((response) => response as ConfiguredRivusDaemonBootstrapResponse)),
84
+ runIds: createUuidRunIds(),
85
+ sleep: (ms) => Effect.promise(() => new Promise<void>((resolve) => setTimeout(resolve, ms))),
86
+ websocketClient: createLazyFeishuWebSocketClient({
87
+ appId: context.config.feishu.appId,
88
+ appSecret: context.config.feishu.appSecret
89
+ })
90
+ })
91
+ );
92
+
93
+ return {
94
+ promptText: (command) => bootstrap.promptText(command),
95
+ replayReceiveMessage: (payload: FeishuReceiveMessagePayload, options?: FeishuReceiveMessageReplayOptions) =>
96
+ bootstrap.runtime.replayReceiveMessage(payload, options),
97
+ running: () => bootstrap.process.running(),
98
+ start: () => bootstrap.process.start(),
99
+ status: () => bootstrap.status(),
100
+ stop: () =>
101
+ Effect.gen(function* () {
102
+ yield* bootstrap.process.stop();
103
+ yield* Effect.tryPromise({
104
+ try: () => sessionRegistry.disposeAll(),
105
+ catch: (error) => error
106
+ });
107
+ })
108
+ };
109
+ }
110
+
111
+ function createLazyFeishuWebSocketClient(options: {
112
+ readonly appId: string;
113
+ readonly appSecret: string;
114
+ }): FeishuWebSocketClient {
115
+ let client:
116
+ | {
117
+ close(): void;
118
+ start(options: Parameters<FeishuWebSocketClient["start"]>[0]): Promise<void> | void;
119
+ }
120
+ | undefined;
121
+
122
+ return {
123
+ start: (startOptions) => {
124
+ client ??= new Lark.WSClient({
125
+ appId: options.appId,
126
+ appSecret: options.appSecret
127
+ });
128
+ return client.start(startOptions);
129
+ },
130
+ close: () => {
131
+ client?.close();
132
+ client = undefined;
133
+ }
134
+ };
135
+ }
136
+
137
+ async function createPiSessionOptions(context: RivusDaemonBootstrapContext): Promise<PiSessionOptions> {
138
+ const authStorage = AuthStorage.create(PI_AUTH_FILE);
139
+ const provider = readProviderFromModel(context.config.pi.model);
140
+
141
+ if (context.config.pi.apiKey) {
142
+ if (!provider) {
143
+ throw new Error("PI_API_KEY requires PI_MODEL in provider/model form, for example zai/glm-5.1");
144
+ }
145
+ authStorage.setRuntimeApiKey(provider, context.config.pi.apiKey);
146
+ }
147
+
148
+ if (context.config.pi.baseUrl) {
149
+ if (!provider) {
150
+ throw new Error("PI_BASE_URL requires PI_MODEL in provider/model form, for example zai/glm-5.1");
151
+ }
152
+ await writeProviderBaseUrlOverride(provider, context.config.pi.baseUrl);
153
+ }
154
+
155
+ const modelRegistry = ModelRegistry.create(authStorage, context.config.pi.baseUrl ? PI_MODELS_FILE : undefined);
156
+ const model = context.config.pi.model ? resolveConfiguredPiModel(modelRegistry, context.config.pi.model) : undefined;
157
+ return {
158
+ authStorage,
159
+ cwd: process.cwd(),
160
+ modelRegistry,
161
+ ...(model ? { model } : {}),
162
+ ...(context.config.pi.thinkingLevel ? { thinkingLevel: context.config.pi.thinkingLevel } : {})
163
+ };
164
+ }
165
+
166
+ function resolveConfiguredPiModel(modelRegistry: ModelRegistry, modelReference: string): PiSessionOptions["model"] {
167
+ const provider = readProviderFromModel(modelReference);
168
+ const modelId = readModelIdFromModel(modelReference);
169
+ if (!provider || !modelId) {
170
+ throw new Error("PI_MODEL must use provider/model form, for example zai/glm-5.1");
171
+ }
172
+
173
+ const model = modelRegistry.find(provider, modelId);
174
+ if (!model) {
175
+ throw new Error(`PI_MODEL ${modelReference} was not found in the Pi model registry`);
176
+ }
177
+ return model as PiSessionOptions["model"];
178
+ }
179
+
180
+ function readProviderFromModel(model: string | undefined): string | undefined {
181
+ const slashIndex = model?.indexOf("/") ?? -1;
182
+ if (!model || slashIndex <= 0) {
183
+ return undefined;
184
+ }
185
+ return model.slice(0, slashIndex);
186
+ }
187
+
188
+ function readModelIdFromModel(model: string | undefined): string | undefined {
189
+ const slashIndex = model?.indexOf("/") ?? -1;
190
+ if (!model || slashIndex < 0 || slashIndex === model.length - 1) {
191
+ return undefined;
192
+ }
193
+ return model.slice(slashIndex + 1);
194
+ }
195
+
196
+ function readOptionalPort(value: string | undefined): number | undefined {
197
+ const trimmed = value?.trim();
198
+ if (!trimmed) {
199
+ return undefined;
200
+ }
201
+
202
+ const port = Number(trimmed);
203
+ if (!Number.isInteger(port) || port < 0 || port > 65_535) {
204
+ throw new Error("RIVUS_STATUS_PORT must be an integer between 0 and 65535");
205
+ }
206
+ return port;
207
+ }
208
+
209
+ async function writeProviderBaseUrlOverride(provider: string, baseUrl: string): Promise<void> {
210
+ await writeFile(
211
+ PI_MODELS_FILE,
212
+ `${JSON.stringify(
213
+ {
214
+ providers: {
215
+ [provider]: {
216
+ baseUrl
217
+ }
218
+ }
219
+ },
220
+ null,
221
+ 2
222
+ )}\n`,
223
+ "utf8"
224
+ );
225
+ }
@@ -0,0 +1,238 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { createHash } from "node:crypto";
3
+ import { join } from "node:path";
4
+
5
+ import { readCurrentWeather } from "./current-weather.mjs";
6
+ import { createHtmlArtifactWriter, createLarkDriveHtmlUploader } from "./html-drive-tools.mjs";
7
+
8
+ const TOOL_IDS = {
9
+ currentWeather: "rivus-example-agents/current-weather",
10
+ larkDriveUploadHtml: "rivus-example-agents/lark-drive-upload-html",
11
+ runtimeInfo: "rivus-example-agents/runtime-info",
12
+ saveNote: "rivus-example-agents/save-note",
13
+ writeHtmlArtifact: "rivus-example-agents/write-html-artifact"
14
+ };
15
+ const ALLOWED_TOOL_IDS = [TOOL_IDS.runtimeInfo, TOOL_IDS.saveNote, TOOL_IDS.currentWeather];
16
+ const WEATHER_INSTRUCTION =
17
+ "遇到当前或今日天气问题时必须调用 current-weather;用户未指定地点时不要猜测,省略 location 并在回答中明确说明工具返回的默认地点。";
18
+ const MEMORY_INSTRUCTION =
19
+ "用户明确要求记住、回忆或遗忘偏好时,必须调用 rivus_memory;propose 会持久化一条可在同 Scope 后续检索的待确认候选,不得声称用户已经确认,也不得声称候选尚未写入。";
20
+ const DAILY_WORD_AUTOMATION_ID = "rivus-example-agents/daily-ielts-word";
21
+ const LANGFUSE_PUBLISHER_SKILL_ID = "rivus-example-agents/langfuse-html-publisher";
22
+ const LANGFUSE_PUBLISHER_SKILL = Object.freeze({
23
+ content: [
24
+ "Use this skill when the user asks for a polished HTML page that must be published to Feishu Drive.",
25
+ "1. Design a responsive, accessible, self-contained HTML document with semantic sections and inline CSS. Do not use external scripts or remote assets.",
26
+ "2. Call write-html-artifact exactly once with the complete document. Do not claim a file exists before the Tool succeeds.",
27
+ "3. Read the returned artifactId, then call lark-drive-upload-html exactly once with that artifactId and a descriptive .html file name.",
28
+ "4. Return the verified Drive URL and a concise summary of the page. A local path alone is not a completed publication.",
29
+ "5. Treat metrics, timings, costs, evaluations, and execution claims as facts only when a Tool result or supplied trace contains them. Clearly label any illustrative data as synthetic.",
30
+ "6. For Langfuse technical facts, use this official-doc reference verified on 2026-07-15: Trace is the root execution and Observations are nestable steps (do not claim unlimited nesting); supported Observation types are event, span, generation, agent, tool, chain, retriever, evaluator, embedding, and guardrail. A current Python example imports get_client and uses with langfuse.start_as_current_observation(as_type=..., name=...) plus observation.update(...), followed by langfuse.flush() for short-lived processes. The standard environment variables are LANGFUSE_SECRET_KEY, LANGFUSE_PUBLIC_KEY, and LANGFUSE_BASE_URL. Never use the removed v2 langfuse.trace(), trace.generation(), or trace.span() APIs, and never shorten the credential names to LANGFUSE_SECRET or LANGFUSE_PUBLIC.",
31
+ "7. Any credential example in the page must use these exact illustrative values: LANGFUSE_SECRET_KEY=sk-lf-... ; LANGFUSE_PUBLIC_KEY=pk-lf-... ; LANGFUSE_BASE_URL=https://jp.cloud.langfuse.com . The values are placeholders, not real credentials.",
32
+ "Official references: https://langfuse.com/docs/observability/data-model ; https://langfuse.com/docs/observability/features/observation-types ; https://langfuse.com/docs/observability/get-started"
33
+ ].join("\n"),
34
+ digest: "sha256:example-langfuse-html-publisher-v5",
35
+ id: LANGFUSE_PUBLISHER_SKILL_ID,
36
+ title: "Langfuse HTML publisher",
37
+ version: "5.0.0"
38
+ });
39
+
40
+ export default {
41
+ manifest: {
42
+ apiVersion: "1",
43
+ id: "rivus-example-agents",
44
+ version: "0.1.0"
45
+ },
46
+ register(registry) {
47
+ registry.registerTool({
48
+ createExecutor: () => ({
49
+ execute: (_input, context) => ({
50
+ agentId: context.agentId,
51
+ instanceId: context.instanceId,
52
+ policyEpoch: context.policyEpoch,
53
+ runId: context.runId
54
+ })
55
+ }),
56
+ description: "Read the trusted Rivus runtime identity for the current run",
57
+ digest: "sha256:example-runtime-info-v1",
58
+ id: TOOL_IDS.runtimeInfo,
59
+ idempotency: "none",
60
+ inputSchema: { additionalProperties: false, properties: {}, type: "object" },
61
+ risk: "observe",
62
+ version: "1.0.0"
63
+ });
64
+ registry.registerTool({
65
+ createExecutor: () => ({ execute: saveNote }),
66
+ description: "Persist a Markdown note in the local Rivus deployment state",
67
+ digest: "sha256:example-save-note-v1",
68
+ id: TOOL_IDS.saveNote,
69
+ idempotency: "required",
70
+ inputSchema: {
71
+ additionalProperties: false,
72
+ properties: {
73
+ text: { minLength: 1, type: "string" },
74
+ title: { minLength: 1, type: "string" }
75
+ },
76
+ required: ["title", "text"],
77
+ type: "object"
78
+ },
79
+ risk: "irreversible",
80
+ version: "1.0.0"
81
+ });
82
+ registry.registerTool({
83
+ createExecutor: () => ({ execute: readCurrentWeather }),
84
+ description:
85
+ "Read current conditions and today's forecast from Open-Meteo. When location is omitted, use the deployment's configured default location.",
86
+ digest: "sha256:example-current-weather-v1",
87
+ id: TOOL_IDS.currentWeather,
88
+ idempotency: "none",
89
+ inputSchema: {
90
+ additionalProperties: false,
91
+ properties: {
92
+ location: {
93
+ description: "City or place name. Omit it when the user did not specify a location.",
94
+ maxLength: 100,
95
+ minLength: 1,
96
+ type: "string"
97
+ }
98
+ },
99
+ type: "object"
100
+ },
101
+ risk: "observe",
102
+ version: "1.0.0"
103
+ });
104
+ registry.registerTool({
105
+ createExecutor: () => createHtmlArtifactWriter(),
106
+ description:
107
+ "Write one complete, self-contained HTML document into the trusted Rivus artifact directory and return its artifactId",
108
+ digest: "sha256:example-write-html-artifact-v1",
109
+ id: TOOL_IDS.writeHtmlArtifact,
110
+ idempotency: "required",
111
+ inputSchema: {
112
+ additionalProperties: false,
113
+ properties: {
114
+ html: {
115
+ description: "Complete HTML document beginning with <!doctype html> and ending with </html>",
116
+ maxLength: 262144,
117
+ minLength: 100,
118
+ type: "string"
119
+ }
120
+ },
121
+ required: ["html"],
122
+ type: "object"
123
+ },
124
+ risk: "mutate",
125
+ version: "1.0.0"
126
+ });
127
+ registry.registerTool({
128
+ createExecutor: () => createLarkDriveHtmlUploader(),
129
+ description:
130
+ "Upload a previously generated HTML artifact to the current user's Feishu Drive root by invoking lark-cli",
131
+ digest: "sha256:example-lark-drive-upload-html-v1",
132
+ id: TOOL_IDS.larkDriveUploadHtml,
133
+ idempotency: "required",
134
+ inputSchema: {
135
+ additionalProperties: false,
136
+ properties: {
137
+ artifactId: {
138
+ description: "artifactId returned by write-html-artifact",
139
+ pattern: "^[a-f0-9]{32}$",
140
+ type: "string"
141
+ },
142
+ name: {
143
+ description: "Safe destination file name ending in .html",
144
+ maxLength: 125,
145
+ minLength: 6,
146
+ pattern: "^[^/\\\\]{1,120}\\.html$",
147
+ type: "string"
148
+ }
149
+ },
150
+ required: ["artifactId", "name"],
151
+ type: "object"
152
+ },
153
+ risk: "mutate",
154
+ version: "1.0.0"
155
+ });
156
+ registry.registerSkill(LANGFUSE_PUBLISHER_SKILL);
157
+ registry.registerAgentProfile({
158
+ displayName: "Rivus Agent A",
159
+ id: "agent-a",
160
+ memory: { scopes: ["agent-private"] },
161
+ model: {},
162
+ skills: { allow: [] },
163
+ systemPrompt: `You are Rivus Agent A. Be concise, analytical, and explicit about evidence. ${WEATHER_INSTRUCTION} ${MEMORY_INSTRUCTION}`,
164
+ tools: { allow: ALLOWED_TOOL_IDS }
165
+ });
166
+ registry.registerAgentProfile({
167
+ displayName: "Rivus Agent B",
168
+ id: "agent-b",
169
+ memory: { scopes: ["agent-private"] },
170
+ model: {},
171
+ skills: { allow: [] },
172
+ systemPrompt: `You are Rivus Agent B. Focus on independent verification and clearly state uncertainty. ${WEATHER_INSTRUCTION} ${MEMORY_INSTRUCTION}`,
173
+ tools: { allow: ALLOWED_TOOL_IDS }
174
+ });
175
+ registry.registerAgentProfile({
176
+ displayName: "Rivus Langfuse publishing demo",
177
+ id: "langfuse-demo",
178
+ memory: { scopes: [] },
179
+ model: {},
180
+ skills: { allow: [LANGFUSE_PUBLISHER_SKILL_ID] },
181
+ systemPrompt: [
182
+ "You are the isolated Rivus Langfuse publishing demo Agent.",
183
+ `Before planning or publishing, read and follow the granted Skill ${LANGFUSE_PUBLISHER_SKILL_ID}.`,
184
+ "Use only Tool results and user-provided evidence for factual execution claims."
185
+ ].join(" "),
186
+ tools: { allow: [TOOL_IDS.writeHtmlArtifact, TOOL_IDS.larkDriveUploadHtml] }
187
+ });
188
+ registry.registerAutomation({
189
+ createInput: ({ occurrence }) => ({
190
+ text: [
191
+ `计划发生时间:${occurrence}。为中文雅思学习者生成且只生成一张今日单词卡。`,
192
+ "选择一个雅思高频但不太基础的英文单词。",
193
+ "必须依次包含:单词、英式和美式 IPA、中文发音提示、词性与中文释义、一个雅思语境英文例句及中文翻译、常用搭配、记忆提示。",
194
+ "最后提供可直接点击收听发音的 Cambridge Dictionary 链接:https://dictionary.cambridge.org/pronunciation/english/<小写单词>。",
195
+ "使用飞书富文本支持的 Markdown 排版:以二级标题开头,字段名加粗,例句、搭配和记忆提示分段或列表展示,发音链接使用 Markdown 链接语法。不要使用表格,不要解释生成过程,也不要输出多个候选词。"
196
+ ].join("\n")
197
+ }),
198
+ id: DAILY_WORD_AUTOMATION_ID,
199
+ profileId: "agent-a",
200
+ requestedSkillIds: [],
201
+ requestedToolIds: []
202
+ });
203
+ }
204
+ };
205
+
206
+ async function saveNote(input, context) {
207
+ if (
208
+ !input ||
209
+ typeof input !== "object" ||
210
+ typeof input.title !== "string" ||
211
+ !input.title.trim() ||
212
+ typeof input.text !== "string" ||
213
+ !input.text.trim() ||
214
+ !context.operationId
215
+ ) {
216
+ throw new Error("save-note requires title, text, and a trusted operation id");
217
+ }
218
+ const directory = join(
219
+ process.cwd(),
220
+ ".rivus",
221
+ "deployment",
222
+ "tool-output",
223
+ createHash("sha256").update(context.agentId).digest("hex").slice(0, 16)
224
+ );
225
+ const fileName = `${context.operationId.replace(/[^a-zA-Z0-9_-]/g, "_")}.md`;
226
+ const filePath = join(directory, fileName);
227
+ const content = `# ${input.title.trim()}\n\n${input.text.trim()}\n`;
228
+ await mkdir(directory, { recursive: true });
229
+ try {
230
+ await writeFile(filePath, content, { encoding: "utf8", flag: "wx" });
231
+ } catch (error) {
232
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST") throw error;
233
+ if ((await readFile(filePath, "utf8")) !== content) {
234
+ throw new Error("operation id is already bound to different note content");
235
+ }
236
+ }
237
+ return { path: `.rivus/deployment/tool-output/${fileName}`, saved: true };
238
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "plugins": [
3
+ {
4
+ "id": "rivus-example-agents",
5
+ "module": "./rivus-agents.plugin.mjs",
6
+ "required": true
7
+ }
8
+ ],
9
+ "agents": [
10
+ {
11
+ "agentId": "langfuse-demo",
12
+ "endpointIds": ["langfuse-demo-feishu"],
13
+ "pluginId": "rivus-example-agents",
14
+ "profileId": "langfuse-demo",
15
+ "skills": { "allow": ["rivus-example-agents/langfuse-html-publisher"] },
16
+ "tools": {
17
+ "allow": ["rivus-example-agents/write-html-artifact", "rivus-example-agents/lark-drive-upload-html"]
18
+ }
19
+ }
20
+ ],
21
+ "endpoints": [
22
+ {
23
+ "id": "langfuse-demo-feishu",
24
+ "agentId": "langfuse-demo",
25
+ "sessionNamespace": "langfuse-demo-v1",
26
+ "credentialRef": "env:RIVUS_AGENT_A_FEISHU",
27
+ "enabled": true,
28
+ "required": true,
29
+ "baseUrl": "https://open.feishu.cn",
30
+ "streamMinIntervalMs": 200,
31
+ "groupPolicy": "ignore-unmentioned"
32
+ }
33
+ ],
34
+ "defaultAgentId": "langfuse-demo",
35
+ "defaultEndpointId": "langfuse-demo-feishu"
36
+ }
@@ -0,0 +1,83 @@
1
+ {
2
+ "plugins": [
3
+ {
4
+ "id": "rivus-example-agents",
5
+ "module": "./rivus-agents.plugin.mjs",
6
+ "required": true
7
+ }
8
+ ],
9
+ "agents": [
10
+ {
11
+ "agentId": "agent-a",
12
+ "endpointIds": ["feishu-agent-a"],
13
+ "memory": { "scopes": ["agent-private"], "tool": true },
14
+ "pluginId": "rivus-example-agents",
15
+ "profileId": "agent-a",
16
+ "skills": { "allow": [] },
17
+ "tools": {
18
+ "allow": [
19
+ "rivus-example-agents/runtime-info",
20
+ "rivus-example-agents/save-note",
21
+ "rivus-example-agents/current-weather"
22
+ ]
23
+ }
24
+ },
25
+ {
26
+ "agentId": "agent-b",
27
+ "endpointIds": ["feishu-agent-b"],
28
+ "memory": { "scopes": ["agent-private"], "tool": true },
29
+ "pluginId": "rivus-example-agents",
30
+ "profileId": "agent-b",
31
+ "skills": { "allow": [] },
32
+ "tools": {
33
+ "allow": [
34
+ "rivus-example-agents/runtime-info",
35
+ "rivus-example-agents/save-note",
36
+ "rivus-example-agents/current-weather"
37
+ ]
38
+ }
39
+ }
40
+ ],
41
+ "automations": [
42
+ {
43
+ "id": "daily-ielts-word",
44
+ "agentId": "agent-a",
45
+ "templateId": "rivus-example-agents/daily-ielts-word",
46
+ "enabled": false,
47
+ "required": true,
48
+ "schedule": "0 10 * * *",
49
+ "timeZone": "Asia/Shanghai",
50
+ "delivery": {
51
+ "endpointId": "feishu-agent-a",
52
+ "targetRef": "env:RIVUS_DAILY_IELTS_WORD_TARGET",
53
+ "targetType": "union_id"
54
+ }
55
+ }
56
+ ],
57
+ "endpoints": [
58
+ {
59
+ "id": "feishu-agent-a",
60
+ "agentId": "agent-a",
61
+ "sessionNamespace": "feishu-agent-a-v1",
62
+ "credentialRef": "env:RIVUS_AGENT_A_FEISHU",
63
+ "enabled": true,
64
+ "required": true,
65
+ "baseUrl": "https://open.feishu.cn",
66
+ "streamMinIntervalMs": 200,
67
+ "groupPolicy": "mention-only"
68
+ },
69
+ {
70
+ "id": "feishu-agent-b",
71
+ "agentId": "agent-b",
72
+ "sessionNamespace": "feishu-agent-b-v1",
73
+ "credentialRef": "env:RIVUS_AGENT_B_FEISHU",
74
+ "enabled": true,
75
+ "required": true,
76
+ "baseUrl": "https://open.feishu.cn",
77
+ "streamMinIntervalMs": 200,
78
+ "groupPolicy": "mention-only"
79
+ }
80
+ ],
81
+ "defaultAgentId": "agent-a",
82
+ "defaultEndpointId": "feishu-agent-a"
83
+ }