@rivus/agent 0.1.1 → 0.4.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,86 @@
1
+ import { readAShareProviderResponse } from "./a-share-provider-response.mjs";
2
+
3
+ const SOURCE = "腾讯证券海外指数公开行情";
4
+ const MAX_AGE_MS = 4 * 24 * 60 * 60 * 1000;
5
+ const INDEXES = Object.freeze([
6
+ { name: "道琼斯", symbol: "usDJI" },
7
+ { name: "纳斯达克", symbol: "usIXIC" },
8
+ { name: "标普 500", symbol: "usINX" }
9
+ ]);
10
+
11
+ export async function readAShareOverseasEvidence({ occurrence }) {
12
+ const url = new URL("https://qt.gtimg.cn/");
13
+ url.searchParams.set("q", INDEXES.map(({ symbol }) => symbol).join(","));
14
+ const responseBody = await readAShareProviderResponse({
15
+ accept: "text/plain",
16
+ maxBytes: 64 * 1024,
17
+ referer: "https://finance.qq.com/",
18
+ source: "a-share overseas evidence",
19
+ url
20
+ });
21
+ const body = new TextDecoder("gbk").decode(responseBody);
22
+ const quotes = INDEXES.map((index) => parseQuote(index, body));
23
+ const asOf = quotes
24
+ .map(({ providerTime }) => providerTime)
25
+ .sort((left, right) => left.localeCompare(right))
26
+ .at(-1);
27
+ if (new Set(quotes.map(({ providerTime }) => providerTime)).size !== 1) {
28
+ throw new Error("a-share overseas evidence timestamps are inconsistent");
29
+ }
30
+ const age = providerClockTime(occurrence) - providerClockTime(asOf);
31
+ if (age < 0 || age > MAX_AGE_MS) throw new Error("a-share overseas evidence is stale for the occurrence");
32
+ return Object.freeze({ asOf, quotes: Object.freeze(quotes), source: SOURCE });
33
+ }
34
+
35
+ function providerClockTime(value) {
36
+ if (value.includes("T")) {
37
+ const parts = new Intl.DateTimeFormat("en-CA", {
38
+ day: "2-digit",
39
+ hour: "2-digit",
40
+ hourCycle: "h23",
41
+ minute: "2-digit",
42
+ month: "2-digit",
43
+ second: "2-digit",
44
+ timeZone: "America/New_York",
45
+ year: "numeric"
46
+ }).formatToParts(new Date(value));
47
+ const read = (type) => parts.find((part) => part.type === type)?.value;
48
+ return Date.parse(
49
+ `${read("year")}-${read("month")}-${read("day")}T${read("hour")}:${read("minute")}:${read("second")}Z`
50
+ );
51
+ }
52
+ return Date.parse(`${value.replace(" ", "T")}Z`);
53
+ }
54
+
55
+ function parseQuote(index, body) {
56
+ const match = body.match(new RegExp(`(?:^|\\n)v_${index.symbol}="([^"]+)";`, "u"));
57
+ if (!match) throw new Error(`a-share overseas evidence is missing ${index.symbol}`);
58
+ const fields = match[1].split("~");
59
+ const providerTime = fields[30];
60
+ if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(providerTime ?? "")) {
61
+ throw new Error(`a-share overseas evidence is missing a timestamp for ${index.symbol}`);
62
+ }
63
+ if (!isValidProviderTimestamp(providerTime)) {
64
+ throw new Error(`a-share overseas evidence has an invalid timestamp for ${index.symbol}`);
65
+ }
66
+ if (fields[35] !== "USD") throw new Error(`a-share overseas evidence has an unexpected currency for ${index.symbol}`);
67
+ return Object.freeze({
68
+ changePercent: readFiniteNumber(fields[32], index.symbol, "change percent"),
69
+ name: index.name,
70
+ price: readFiniteNumber(fields[3], index.symbol, "price"),
71
+ providerTime,
72
+ symbol: index.symbol
73
+ });
74
+ }
75
+
76
+ function isValidProviderTimestamp(value) {
77
+ const normalized = value.replace(" ", "T");
78
+ const timestamp = Date.parse(`${normalized}Z`);
79
+ return Number.isFinite(timestamp) && new Date(timestamp).toISOString().slice(0, 19) === normalized;
80
+ }
81
+
82
+ function readFiniteNumber(value, symbol, field) {
83
+ const number = Number(value);
84
+ if (!Number.isFinite(number)) throw new Error(`a-share overseas evidence is missing ${field} for ${symbol}`);
85
+ return number;
86
+ }
@@ -0,0 +1,145 @@
1
+ import { readAShareProviderResponse } from "./a-share-provider-response.mjs";
2
+
3
+ const LOOKBACK_MS = 3 * 24 * 60 * 60 * 1000;
4
+
5
+ export async function readASharePolicyEvidence({ occurrence }) {
6
+ const occurrenceTime = Date.parse(occurrence);
7
+ const readers = [
8
+ { read: readPbcHeadlines, source: "中国人民银行" },
9
+ { read: readCsrcHeadlines, source: "中国证监会" }
10
+ ];
11
+ const settled = await Promise.allSettled(readers.map(({ read }) => read()));
12
+ const available = settled.flatMap((result, index) =>
13
+ result.status === "fulfilled" ? [{ items: result.value, source: readers[index].source }] : []
14
+ );
15
+ if (available.length === 0) throw new Error("a-share policy evidence sources are unavailable");
16
+ const items = available
17
+ .flatMap(({ items }) => items)
18
+ .filter(({ publishedAt }) => {
19
+ const publishedTime = Date.parse(publishedAt);
20
+ return publishedTime <= occurrenceTime && publishedTime >= occurrenceTime - LOOKBACK_MS;
21
+ })
22
+ .sort((left, right) => right.publishedAt.localeCompare(left.publishedAt))
23
+ .slice(0, 4);
24
+ const sources = available.map(({ source }) => source);
25
+ const unavailableSources = settled.flatMap((result, index) =>
26
+ result.status === "rejected" ? [readers[index].source] : []
27
+ );
28
+ return Object.freeze({
29
+ items: Object.freeze(items),
30
+ sources: Object.freeze(sources),
31
+ unavailableSources: Object.freeze(unavailableSources)
32
+ });
33
+ }
34
+
35
+ async function readPbcHeadlines() {
36
+ const url = new URL("https://www.pbc.gov.cn/goutongjiaoliu/113456/113469/index.html");
37
+ const body = await readText(url, "中国人民银行");
38
+ const items = [];
39
+ const pattern =
40
+ /<a\s+([^>]*\bistitle="true"[^>]*)>[\s\S]*?<\/a>\s*<\/font>\s*<span[^>]*>\s*(\d{4}-\d{2}-\d{2})\s*<\/span>/giu;
41
+ for (const match of body.matchAll(pattern)) {
42
+ const href = readHtmlAttribute(match[1], "href");
43
+ const title = decodeHtml(readHtmlAttribute(match[1], "title"));
44
+ if (!href || !title || title.length > 200) continue;
45
+ const itemUrl = normalizeSourceUrl(href, url, "www.pbc.gov.cn");
46
+ if (!itemUrl) continue;
47
+ items.push(
48
+ Object.freeze({
49
+ publishedAt: readPbcPublishedAt(itemUrl, match[2]),
50
+ source: "中国人民银行",
51
+ title,
52
+ url: itemUrl
53
+ })
54
+ );
55
+ if (items.length === 2) break;
56
+ }
57
+ if (items.length === 0) throw new Error("PBC policy evidence contains no valid headlines");
58
+ return Object.freeze(items);
59
+ }
60
+
61
+ function readPbcPublishedAt(itemUrl, displayedDate) {
62
+ const timestamp = new URL(itemUrl).pathname.match(/\/(\d{14})\d*\/index\.html$/u)?.[1];
63
+ if (!timestamp) return `${displayedDate}T23:59:59+08:00`;
64
+ return `${timestamp.replace(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/u, "$1-$2-$3T$4:$5:$6")}+08:00`;
65
+ }
66
+
67
+ async function readCsrcHeadlines() {
68
+ const url = new URL("https://www.csrc.gov.cn/searchList/a1a078ee0bc54721ab6b148884c784a8");
69
+ url.searchParams.set("_isAgg", "true");
70
+ url.searchParams.set("_isJson", "true");
71
+ url.searchParams.set("_pageSize", "5");
72
+ url.searchParams.set("_template", "index");
73
+ url.searchParams.set("_rangeTimeGte", "");
74
+ url.searchParams.set("_channelName", "");
75
+ url.searchParams.set("page", "1");
76
+ const body = await readText(url, "中国证监会", 256 * 1024);
77
+ let payload;
78
+ try {
79
+ payload = JSON.parse(body);
80
+ } catch (cause) {
81
+ throw new Error("CSRC policy evidence is not valid JSON", { cause });
82
+ }
83
+ const results = Array.isArray(payload?.data?.results) ? payload.data.results : undefined;
84
+ if (!results) throw new Error("CSRC policy evidence contains no result list");
85
+ const items = results
86
+ .flatMap((row) => {
87
+ if (
88
+ typeof row?.title !== "string" ||
89
+ !row.title ||
90
+ row.title.length > 200 ||
91
+ typeof row?.url !== "string" ||
92
+ !/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(row?.publishedTimeStr ?? "")
93
+ ) {
94
+ return [];
95
+ }
96
+ const itemUrl = normalizeSourceUrl(row.url, url, "www.csrc.gov.cn");
97
+ if (!itemUrl) return [];
98
+ return [
99
+ Object.freeze({
100
+ publishedAt: `${row.publishedTimeStr.replace(" ", "T")}+08:00`,
101
+ source: "中国证监会",
102
+ title: row.title,
103
+ url: itemUrl
104
+ })
105
+ ];
106
+ })
107
+ .slice(0, 2);
108
+ if (items.length === 0) throw new Error("CSRC policy evidence contains no valid headlines");
109
+ return Object.freeze(items);
110
+ }
111
+
112
+ async function readText(url, source, maxBytes = 128 * 1024) {
113
+ const body = await readAShareProviderResponse({
114
+ accept: "text/html,application/json",
115
+ maxBytes,
116
+ referer: `${url.origin}/`,
117
+ source: `${source} policy evidence`,
118
+ url
119
+ });
120
+ return body.toString("utf8");
121
+ }
122
+
123
+ function readHtmlAttribute(attributes, name) {
124
+ return attributes.match(new RegExp(`\\b${name}="([^"]+)"`, "iu"))?.[1];
125
+ }
126
+
127
+ function normalizeSourceUrl(value, baseUrl, expectedHostname) {
128
+ let url;
129
+ try {
130
+ url = new URL(value, baseUrl);
131
+ } catch {
132
+ return undefined;
133
+ }
134
+ if (url.protocol !== "https:" || url.hostname !== expectedHostname) return undefined;
135
+ return url.href;
136
+ }
137
+
138
+ function decodeHtml(value) {
139
+ return value
140
+ .replaceAll("&amp;", "&")
141
+ .replaceAll("&quot;", '"')
142
+ .replaceAll("&#39;", "'")
143
+ .replaceAll("&lt;", "<")
144
+ .replaceAll("&gt;", ">");
145
+ }
@@ -0,0 +1,21 @@
1
+ import { readBoundedHttpsResponse } from "./https-response-reader.mjs";
2
+
3
+ export async function readAShareProviderResponse({ accept, maxBytes, referer, source, url }) {
4
+ let response;
5
+ try {
6
+ response = await readBoundedHttpsResponse(url, {
7
+ headers: {
8
+ accept,
9
+ referer,
10
+ "user-agent": "rivus-a-share-market-briefing/1.1"
11
+ },
12
+ maxBytes
13
+ });
14
+ } catch (cause) {
15
+ throw new Error(`${source} request failed`, { cause });
16
+ }
17
+ if (response.status < 200 || response.status >= 300) {
18
+ throw new Error(`${source} request failed with HTTP ${response.status}`);
19
+ }
20
+ return response.body;
21
+ }
@@ -0,0 +1,70 @@
1
+ import { dateInShanghai } from "./a-share-market-date.mjs";
2
+ import { readAShareProviderResponse } from "./a-share-provider-response.mjs";
3
+
4
+ const SOURCE = "东方财富行业板块公开行情";
5
+
6
+ export async function readAShareSectorEvidence({ marketDate }) {
7
+ const [leaders, laggards] = await Promise.all([readSectorRanking(true), readSectorRanking(false)]);
8
+ const sectors = [...leaders, ...laggards];
9
+ const asOf = sectors
10
+ .map(({ asOf }) => asOf)
11
+ .sort((left, right) => left.localeCompare(right))
12
+ .at(-1);
13
+ if (!sectors.every((sector) => dateInShanghai(new Date(sector.asOf)) === marketDate)) {
14
+ throw new Error("a-share sector evidence is stale for the market date");
15
+ }
16
+ return Object.freeze({ asOf, laggards, leaders, source: SOURCE });
17
+ }
18
+
19
+ async function readSectorRanking(descending) {
20
+ const url = new URL("https://push2delay.eastmoney.com/api/qt/clist/get");
21
+ url.searchParams.set("pn", "1");
22
+ url.searchParams.set("pz", "3");
23
+ url.searchParams.set("po", descending ? "1" : "0");
24
+ url.searchParams.set("np", "1");
25
+ url.searchParams.set("fltt", "2");
26
+ url.searchParams.set("invt", "2");
27
+ url.searchParams.set("fid", "f3");
28
+ url.searchParams.set("fs", "m:90+t:2");
29
+ url.searchParams.set("fields", "f12,f14,f3,f124");
30
+ const body = await readAShareProviderResponse({
31
+ accept: "application/json",
32
+ maxBytes: 64 * 1024,
33
+ referer: "https://quote.eastmoney.com/",
34
+ source: "a-share sector evidence",
35
+ url
36
+ });
37
+ return parseSectorRanking(body.toString("utf8"));
38
+ }
39
+
40
+ function parseSectorRanking(body) {
41
+ let payload;
42
+ try {
43
+ payload = JSON.parse(body);
44
+ } catch (cause) {
45
+ throw new Error("a-share sector evidence is not valid JSON", { cause });
46
+ }
47
+ const rows = payload?.rc === 0 && Array.isArray(payload?.data?.diff) ? payload.data.diff : undefined;
48
+ if (!rows || rows.length !== 3) throw new Error("a-share sector evidence must contain three ranked industries");
49
+ return Object.freeze(
50
+ rows.map((row) => {
51
+ if (
52
+ typeof row?.f12 !== "string" ||
53
+ !row.f12 ||
54
+ typeof row?.f14 !== "string" ||
55
+ !row.f14 ||
56
+ !Number.isFinite(row?.f3) ||
57
+ !Number.isSafeInteger(row?.f124) ||
58
+ row.f124 <= 0
59
+ ) {
60
+ throw new Error("a-share sector evidence contains an invalid industry record");
61
+ }
62
+ return Object.freeze({
63
+ asOf: new Date(row.f124 * 1000).toISOString(),
64
+ changePercent: row.f3,
65
+ id: row.f12,
66
+ name: row.f14
67
+ });
68
+ })
69
+ );
70
+ }
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ createAcpAgentServer,
5
+ createAcpPermissionBridge,
6
+ createAcpStdioAgentLoop,
7
+ serveAcpAgentOnStdio
8
+ } from "@rivus/agent/acp";
9
+
10
+ const command = process.env.RIVUS_ACP_SERVER_COMMAND?.trim();
11
+ if (!command) throw new Error("RIVUS_ACP_SERVER_COMMAND is required and must resolve to an explicit executable");
12
+
13
+ const workingDirectory = process.env.RIVUS_ACP_WORKING_DIRECTORY?.trim() || process.cwd();
14
+ const permissionBridge = createAcpPermissionBridge();
15
+ const downstream = createAcpStdioAgentLoop({
16
+ arguments: parseArguments(process.env.RIVUS_ACP_SERVER_ARGUMENTS),
17
+ command,
18
+ environment: selectEnvironment(process.env.RIVUS_ACP_SERVER_ENV_KEYS),
19
+ onStderr: (text) => process.stderr.write(text),
20
+ permissionPolicy: permissionBridge.policy,
21
+ workingDirectory
22
+ });
23
+ const server = createAcpAgentServer({
24
+ agentName: "rivus-acp-proxy",
25
+ loop: downstream.loop,
26
+ permissionBridge,
27
+ workingDirectory
28
+ });
29
+
30
+ try {
31
+ await serveAcpAgentOnStdio(server);
32
+ } finally {
33
+ await downstream.dispose();
34
+ }
35
+
36
+ function parseArguments(value) {
37
+ if (!value?.trim()) return [];
38
+ const parsed = JSON.parse(value);
39
+ if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
40
+ throw new Error("RIVUS_ACP_SERVER_ARGUMENTS must be a JSON array of strings");
41
+ }
42
+ return parsed;
43
+ }
44
+
45
+ function selectEnvironment(value) {
46
+ const selected = {};
47
+ for (const key of value
48
+ ?.split(",")
49
+ .map((item) => item.trim())
50
+ .filter(Boolean) ?? []) {
51
+ const environmentValue = process.env[key];
52
+ if (environmentValue !== undefined) selected[key] = environmentValue;
53
+ }
54
+ return selected;
55
+ }
@@ -1,4 +1,4 @@
1
- import { get } from "node:https";
1
+ import { readBoundedHttpsResponse } from "./https-response-reader.mjs";
2
2
 
3
3
  export async function readCurrentWeather(input) {
4
4
  if (input !== undefined && (typeof input !== "object" || input === null || Array.isArray(input))) {
@@ -60,7 +60,10 @@ export async function readCurrentWeather(input) {
60
60
  async function fetchJson(url, operation) {
61
61
  let response;
62
62
  try {
63
- response = await requestJson(url);
63
+ response = await readBoundedHttpsResponse(url, {
64
+ headers: { accept: "application/json" },
65
+ maxBytes: 1024 * 1024
66
+ });
64
67
  } catch (error) {
65
68
  throw new Error(`${operation} failed: ${error instanceof Error ? error.message : String(error)}`, {
66
69
  cause: error
@@ -71,7 +74,7 @@ async function fetchJson(url, operation) {
71
74
  }
72
75
  let value;
73
76
  try {
74
- value = JSON.parse(response.body);
77
+ value = JSON.parse(response.body.toString("utf8"));
75
78
  } catch {
76
79
  throw new Error(`${operation} returned invalid JSON`);
77
80
  }
@@ -81,35 +84,6 @@ async function fetchJson(url, operation) {
81
84
  return value;
82
85
  }
83
86
 
84
- function requestJson(url) {
85
- return new Promise((resolve, reject) => {
86
- const request = get(
87
- url,
88
- {
89
- family: 4,
90
- headers: { accept: "application/json" },
91
- signal: AbortSignal.timeout(10_000)
92
- },
93
- (response) => {
94
- const status = response.statusCode ?? 0;
95
- response.once("error", reject);
96
- if (status < 200 || status >= 300) {
97
- response.resume();
98
- resolve({ body: "", status });
99
- return;
100
- }
101
- let body = "";
102
- response.setEncoding("utf8");
103
- response.on("data", (chunk) => {
104
- body += chunk;
105
- });
106
- response.once("end", () => resolve({ body, status }));
107
- }
108
- );
109
- request.once("error", reject);
110
- });
111
- }
112
-
113
87
  function readNumber(value, field) {
114
88
  if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`current-weather missing ${field}`);
115
89
  return value;
@@ -0,0 +1,36 @@
1
+ import { get } from "node:https";
2
+
3
+ export function readBoundedHttpsResponse(url, options) {
4
+ return new Promise((resolve, reject) => {
5
+ const request = get(
6
+ url,
7
+ {
8
+ family: 4,
9
+ headers: options.headers,
10
+ signal: AbortSignal.timeout(options.timeoutMs ?? 10_000)
11
+ },
12
+ (response) => {
13
+ const status = response.statusCode ?? 0;
14
+ response.once("error", reject);
15
+ if (status < 200 || status >= 300) {
16
+ response.resume();
17
+ resolve({ body: Buffer.alloc(0), status });
18
+ return;
19
+ }
20
+ const chunks = [];
21
+ let length = 0;
22
+ response.on("data", (chunk) => {
23
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
24
+ length += buffer.length;
25
+ if (length > options.maxBytes) {
26
+ request.destroy(new Error(`HTTPS response exceeds ${options.maxBytes} bytes`));
27
+ return;
28
+ }
29
+ chunks.push(buffer);
30
+ });
31
+ response.once("end", () => resolve({ body: Buffer.concat(chunks), status }));
32
+ }
33
+ );
34
+ request.once("error", reject);
35
+ });
36
+ }
@@ -15,7 +15,7 @@ import {
15
15
  createAgentHarness,
16
16
  createAgentHarnessPooledRuntime,
17
17
  createRivusMemoryToolDescriptor,
18
- createConfiguredFeishuMarkdownMessageSender,
18
+ createConfiguredFeishuAutomationCardSender,
19
19
  createConfiguredFeishuCardKitPublisher,
20
20
  createConfiguredFeishuCardKitTargetPreparation,
21
21
  createConfiguredFeishuHumanInteractionPresenter,
@@ -113,7 +113,7 @@ export async function createRivusDeploymentAdapters(context: RivusDeploymentBoot
113
113
  pi: {}
114
114
  };
115
115
  const openApiClient = createOpenApiClient(config);
116
- const sender = createConfiguredFeishuMarkdownMessageSender({
116
+ const sender = createConfiguredFeishuAutomationCardSender({
117
117
  client: openApiClient,
118
118
  config
119
119
  });
@@ -3,21 +3,31 @@ import { createHash } from "node:crypto";
3
3
  import { join } from "node:path";
4
4
 
5
5
  import { readCurrentWeather } from "./current-weather.mjs";
6
+ import { readAShareMarketBriefing } from "./a-share-market-briefing.mjs";
6
7
  import { createHtmlArtifactWriter, createLarkDriveHtmlUploader } from "./html-drive-tools.mjs";
7
8
 
8
9
  const TOOL_IDS = {
10
+ aShareMarketBriefing: "rivus-example-agents/a-share-market-briefing",
9
11
  currentWeather: "rivus-example-agents/current-weather",
10
12
  larkDriveUploadHtml: "rivus-example-agents/lark-drive-upload-html",
11
13
  runtimeInfo: "rivus-example-agents/runtime-info",
12
14
  saveNote: "rivus-example-agents/save-note",
13
15
  writeHtmlArtifact: "rivus-example-agents/write-html-artifact"
14
16
  };
15
- const ALLOWED_TOOL_IDS = [TOOL_IDS.runtimeInfo, TOOL_IDS.saveNote, TOOL_IDS.currentWeather];
17
+ const AGENT_A_TOOL_IDS = [
18
+ TOOL_IDS.runtimeInfo,
19
+ TOOL_IDS.saveNote,
20
+ TOOL_IDS.currentWeather,
21
+ TOOL_IDS.aShareMarketBriefing
22
+ ];
23
+ const AGENT_B_TOOL_IDS = [TOOL_IDS.runtimeInfo, TOOL_IDS.saveNote, TOOL_IDS.currentWeather];
16
24
  const WEATHER_INSTRUCTION =
17
25
  "遇到当前或今日天气问题时必须调用 current-weather;用户未指定地点时不要猜测,省略 location 并在回答中明确说明工具返回的默认地点。";
18
26
  const MEMORY_INSTRUCTION =
19
27
  "用户明确要求记住、回忆或遗忘偏好时,必须调用 rivus_memory;propose 会持久化一条可在同 Scope 后续检索的待确认候选,不得声称用户已经确认,也不得声称候选尚未写入。";
20
28
  const DAILY_WORD_AUTOMATION_ID = "rivus-example-agents/daily-ielts-word";
29
+ const A_SHARE_PRE_MARKET_AUTOMATION_ID = "rivus-example-agents/a-share-pre-market";
30
+ const A_SHARE_POST_MARKET_AUTOMATION_ID = "rivus-example-agents/a-share-post-market";
21
31
  const LANGFUSE_PUBLISHER_SKILL_ID = "rivus-example-agents/langfuse-html-publisher";
22
32
  const LANGFUSE_PUBLISHER_SKILL = Object.freeze({
23
33
  content: [
@@ -101,6 +111,25 @@ export default {
101
111
  risk: "observe",
102
112
  version: "1.0.0"
103
113
  });
114
+ registry.registerTool({
115
+ createExecutor: () => ({ execute: readAShareMarketBriefing }),
116
+ description:
117
+ "Read a bounded, evidence-backed A-share pre-market or post-market analysis briefing with attributed facts, labeled inferences, source time, and stale-data handling",
118
+ digest: "sha256:example-a-share-market-briefing-v3",
119
+ id: TOOL_IDS.aShareMarketBriefing,
120
+ idempotency: "none",
121
+ inputSchema: {
122
+ additionalProperties: false,
123
+ properties: {
124
+ occurrence: { format: "date-time", type: "string" },
125
+ session: { enum: ["pre-market", "post-market"], type: "string" }
126
+ },
127
+ required: ["session", "occurrence"],
128
+ type: "object"
129
+ },
130
+ risk: "observe",
131
+ version: "1.1.0"
132
+ });
104
133
  registry.registerTool({
105
134
  createExecutor: () => createHtmlArtifactWriter(),
106
135
  description:
@@ -161,7 +190,7 @@ export default {
161
190
  model: {},
162
191
  skills: { allow: [] },
163
192
  systemPrompt: `You are Rivus Agent A. Be concise, analytical, and explicit about evidence. ${WEATHER_INSTRUCTION} ${MEMORY_INSTRUCTION}`,
164
- tools: { allow: ALLOWED_TOOL_IDS }
193
+ tools: { allow: AGENT_A_TOOL_IDS }
165
194
  });
166
195
  registry.registerAgentProfile({
167
196
  displayName: "Rivus Agent B",
@@ -170,7 +199,7 @@ export default {
170
199
  model: {},
171
200
  skills: { allow: [] },
172
201
  systemPrompt: `You are Rivus Agent B. Focus on independent verification and clearly state uncertainty. ${WEATHER_INSTRUCTION} ${MEMORY_INSTRUCTION}`,
173
- tools: { allow: ALLOWED_TOOL_IDS }
202
+ tools: { allow: AGENT_B_TOOL_IDS }
174
203
  });
175
204
  registry.registerAgentProfile({
176
205
  displayName: "Rivus Langfuse publishing demo",
@@ -200,9 +229,32 @@ export default {
200
229
  requestedSkillIds: [],
201
230
  requestedToolIds: []
202
231
  });
232
+ registry.registerAutomation(createAShareAutomation(A_SHARE_PRE_MARKET_AUTOMATION_ID, "pre-market"));
233
+ registry.registerAutomation(createAShareAutomation(A_SHARE_POST_MARKET_AUTOMATION_ID, "post-market"));
203
234
  }
204
235
  };
205
236
 
237
+ function createAShareAutomation(id, session) {
238
+ const label = session === "pre-market" ? "盘前" : "盘后";
239
+ return {
240
+ createInput: ({ occurrence }) => {
241
+ const parameters = JSON.stringify({ session, occurrence });
242
+ return {
243
+ text: [
244
+ `计划发生时间:${occurrence}。生成 A 股${label}简报。`,
245
+ `必须且只调用一次 ${TOOL_IDS.aShareMarketBriefing},参数严格使用:${parameters}。`,
246
+ "工具返回的 markdown 字段已区分市场事实和分析推断并列出来源,就是最终交付正文;必须原样输出,不增加开场、解释、预测、荐股、因果归因或任何工具结果之外的数字。",
247
+ "如果工具失败,不得估算或补全行情,让本次 Automation 明确失败以便按同一 Tick 重试。"
248
+ ].join("\n")
249
+ };
250
+ },
251
+ id,
252
+ profileId: "agent-a",
253
+ requestedSkillIds: [],
254
+ requestedToolIds: [TOOL_IDS.aShareMarketBriefing]
255
+ };
256
+ }
257
+
206
258
  async function saveNote(input, context) {
207
259
  if (
208
260
  !input ||
@@ -0,0 +1,45 @@
1
+ import { readCurrentWeather } from "./current-weather.mjs";
2
+
3
+ const TOOL_ID = "rivus-starter/current-weather";
4
+
5
+ export default {
6
+ manifest: {
7
+ apiVersion: "1",
8
+ id: "rivus-starter",
9
+ version: "1.0.0"
10
+ },
11
+ register(registry) {
12
+ registry.registerTool({
13
+ createExecutor: () => ({ execute: readCurrentWeather }),
14
+ description:
15
+ "Read current conditions and today's forecast from Open-Meteo, using the configured default when location is omitted",
16
+ digest: "sha256:rivus-starter-current-weather-v1",
17
+ id: TOOL_ID,
18
+ idempotency: "none",
19
+ inputSchema: {
20
+ additionalProperties: false,
21
+ properties: {
22
+ location: {
23
+ description: "City or place name. Omit it when the user did not specify one.",
24
+ maxLength: 100,
25
+ minLength: 1,
26
+ type: "string"
27
+ }
28
+ },
29
+ type: "object"
30
+ },
31
+ risk: "observe",
32
+ version: "1.0.0"
33
+ });
34
+ registry.registerAgentProfile({
35
+ displayName: "Rivus Agent",
36
+ id: "agent-a",
37
+ memory: { scopes: [] },
38
+ model: {},
39
+ skills: { allow: [] },
40
+ systemPrompt:
41
+ "You are a concise local assistant. For current or today's weather, call current-weather. If the user did not name a location, omit location and report the Tool's resolved place.",
42
+ tools: { allow: [TOOL_ID] }
43
+ });
44
+ }
45
+ };