@rivus/agent 0.3.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,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
+ }
@@ -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 ||
@@ -18,7 +18,8 @@
18
18
  "allow": [
19
19
  "rivus-example-agents/runtime-info",
20
20
  "rivus-example-agents/save-note",
21
- "rivus-example-agents/current-weather"
21
+ "rivus-example-agents/current-weather",
22
+ "rivus-example-agents/a-share-market-briefing"
22
23
  ]
23
24
  }
24
25
  },
@@ -52,6 +53,34 @@
52
53
  "targetRef": "env:RIVUS_DAILY_IELTS_WORD_TARGET",
53
54
  "targetType": "union_id"
54
55
  }
56
+ },
57
+ {
58
+ "id": "a-share-pre-market",
59
+ "agentId": "agent-a",
60
+ "templateId": "rivus-example-agents/a-share-pre-market",
61
+ "enabled": false,
62
+ "required": true,
63
+ "schedule": "0 9 * * 1-5",
64
+ "timeZone": "Asia/Shanghai",
65
+ "delivery": {
66
+ "endpointId": "feishu-agent-a",
67
+ "targetRef": "env:RIVUS_A_SHARE_BRIEFING_TARGET",
68
+ "targetType": "union_id"
69
+ }
70
+ },
71
+ {
72
+ "id": "a-share-post-market",
73
+ "agentId": "agent-a",
74
+ "templateId": "rivus-example-agents/a-share-post-market",
75
+ "enabled": false,
76
+ "required": true,
77
+ "schedule": "0 16 * * 1-5",
78
+ "timeZone": "Asia/Shanghai",
79
+ "delivery": {
80
+ "endpointId": "feishu-agent-a",
81
+ "targetRef": "env:RIVUS_A_SHARE_BRIEFING_TARGET",
82
+ "targetType": "union_id"
83
+ }
55
84
  }
56
85
  ],
57
86
  "endpoints": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rivus/agent",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "A local agent daemon core built around a usable agent harness and domain events.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,6 +30,10 @@
30
30
  "types": "./dist/index.d.ts",
31
31
  "import": "./dist/index.js"
32
32
  },
33
+ "./acp": {
34
+ "types": "./dist/acp.d.ts",
35
+ "import": "./dist/acp.js"
36
+ },
33
37
  "./testing": {
34
38
  "types": "./dist/testing/index.d.ts",
35
39
  "import": "./dist/testing/index.js"
@@ -47,7 +51,7 @@
47
51
  "access": "public"
48
52
  },
49
53
  "scripts": {
50
- "audit": "npm audit --audit-level=high",
54
+ "audit": "npm audit --omit=dev --audit-level=high",
51
55
  "build": "vp pack",
52
56
  "changeset": "changeset",
53
57
  "changeset:status": "changeset status",
@@ -78,10 +82,14 @@
78
82
  "typebox": "^1.1.38"
79
83
  },
80
84
  "peerDependencies": {
85
+ "@agentclientprotocol/sdk": "^1.3.0",
81
86
  "@earendil-works/pi-coding-agent": "0.80.6",
82
87
  "@larksuiteoapi/node-sdk": "^1.70.0"
83
88
  },
84
89
  "peerDependenciesMeta": {
90
+ "@agentclientprotocol/sdk": {
91
+ "optional": true
92
+ },
85
93
  "@earendil-works/pi-coding-agent": {
86
94
  "optional": true
87
95
  },
@@ -90,6 +98,7 @@
90
98
  }
91
99
  },
92
100
  "devDependencies": {
101
+ "@agentclientprotocol/sdk": "^1.3.0",
93
102
  "@changesets/cli": "^2.31.1",
94
103
  "@earendil-works/pi-coding-agent": "0.80.6",
95
104
  "@larksuiteoapi/node-sdk": "1.71.1",