@rivus/agent 0.16.1 → 0.16.6

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.
package/README.md CHANGED
@@ -1,1080 +1,4 @@
1
- # Rivus Agent
1
+ # @rivus/agent
2
2
 
3
- A small local agent daemon core for experimenting with a Pi-backed agent loop and Feishu IM integration.
4
-
5
- Documentation: [English](docs/en/README.md) | [简体中文](docs/zh-CN/README.md)
6
-
7
- The first milestone is intentionally narrow: make the agent loop and agent harness layer pleasant to use before adding transport adapters. The core is built around DDD-style domain events and Effect-powered use cases.
8
-
9
- ## Current Status
10
-
11
- - TypeScript ESM package.
12
- - Public Plugin SDK with atomic Plugin registration, immutable Catalog snapshots, deterministic Agent Deployment resolution, an explicit Required/Optional Plugin Loader, safe Node module resolution from a Deployment Root, and a manifest-driven Multi-agent Deployment Daemon. Concrete Agent products remain external packages.
13
- - Revisioned Pi native Runtime Tool grants with a closed seven-Tool vocabulary, exact Profile ∩ Deployment narrowing, fail-closed defaults, and explicit full capability only in the Personal Home starter. Pi Skill discovery accepts an empty catalog and is limited to user-global plus explicit Project Space roots; project-ambient resources stay disabled.
14
- - Host-bound Workspace Instructions API and built-in `AGENTS.md` provider with deterministic root-to-target discovery, immutable source digests, nested mutation refresh diagnostics, UTF-8/path/symlink checks, and whole-source byte budgeting. The deployment example injects the root View into Pi while Tool-target refresh remains enforced by the provider boundary.
15
- - Host-issued invocation authority and a call-time Tool Broker that checks exact grants, pinned versions, online revocation, stable operation IDs, durable side-effect fencing, and one-time approval receipts bound to canonical input digests. The Pi Tool Proxy and Feishu Human Interaction gateway are wired in the deployment example.
16
- - Multi-agent Host primitives with trusted Endpoint bindings, stable Runtime Generation/Instance identities, lazy per-instance Runtime pooling, isolated active-run gates, and restart restoration from persisted registry records.
17
- - Scoped Agent Memory with a single-writer append-only JSONL adapter, explicit Profile/Deployment grants, Host-bound tenant/subject/conversation/project/audience identity, and a provider-neutral `rivus_memory` Tool wired into the Pi manifest Runtime. Agent-private proposals survive process replacement; group invocations may access only Conversation and confirmed Project Memory, and Shared Profile reads expose only confirmed records. Bound Project Memory is recalled automatically with deterministic limits; durable confirmation UX, typed Shared Profile fields, full provenance/retention metadata, and durable Compaction are still separate work.
18
- - Manifest-driven fixed-time Agent Automations with daily or weekday-only schedules, stable Mandate/Tick identity, per-template Tool grant narrowing, isolated Runtime and invocation authority, a durable single-writer JSON Tick adapter that records the actual Agent Run, generated-before-delivery recovery, and idempotent proactive Feishu interactive-card delivery. The first Markdown heading becomes the card title while the remaining Markdown stays scannable in the card body. Revisioned Plugin State and the general Delivery Outbox remain composable domain primitives.
19
- - Opt-in background Agent Sessions (`manifest.backgroundSessions`): the model decides from ordinary natural language that work should continue in the background, calls the generic `background.start` Host Tool, and the foreground Run and card end promptly. A deployment-owned supervisor runs bounded steps on an isolated Runtime lane per Agent with the exact Agent/Profile/Project Space/Skills/Memory policy and a grant narrowed to exclude `background.start`; sessions wait durably (`background.wait`), wake, receive user input once (`background.send`), stop safely, and survive daemon restart on the same stable session and Pi transcript. Progress and one terminal result are delivered idempotently to the trusted source conversation. ACP runtimes reach the same Host Tools through a built-in MCP bridge (`@rivus/agent/mcp`), so Pi and ACP backends share the identical domain, supervisor, persistence, status, and delivery semantics. The Runtime contains no business scenario, meeting, or job code — no pre-model intent router exists.
20
- - Peer Delegation and Subagent coordination primitives with explicit edges, hop/budget limits, monotonic ToolGrantSet intersection, isolated child authority, denied ambient sender/private-memory/Shell capabilities, and parent cancellation cascading.
21
- - `@rivus/agent/testing` Conformance TestKit with a reusable Fake Plugin and checks for registration/reference failures, ToolGrantSet expansion, and lifecycle disposal leaks.
22
- - Effect-based `AgentHarness` use case with result-oriented `prompt()`, turn-oriented `promptTurn()`, event-oriented `stream()`, text-oriented `streamText()`, event-handling `promptWithEvents()`, active-run/run/session state and transcript lookup APIs, and a default runtime factory.
23
- - Session-first `AgentRuntime` convenience facade that keeps the Effect-first harness and Promise client together with a default main session.
24
- - Pure domain events and reducer for agent run state, including separate assistant text and thinking projections.
25
- - Optional Effect-based domain event sinks on `AgentHarness`, plus a JSONL event log and run/session history/transcript replay.
26
- - Production runtime helpers for system time and UUID run ids, restored run-state seeding, plus deterministic testing fakes.
27
- - A small `AgentLoop` port that future Pi SDK and RPC adapters can implement, including cancellation signals plus same-session state, transcript, and conversation-message handoff, plus helpers for async event generators, simple text generators, and string shorthand text deltas.
28
- - A Pi SDK loop adapter that maps `AgentSession.subscribe()` text, thinking, and tool events into daemon loop events, aborts active Pi work when the loop input signal is aborted, and accepts SDK `args` plus hook/RPC-shaped `input` tool payloads.
29
- - Feishu message/card-action intake helpers, callback toast plus card-update responses, exact run cancellation commands, trusted-operator Human Interaction resolution, stable session-key construction, and stream-action projection.
30
- - A Pi session registry that reuses sessions by stable `SessionKey`.
31
- - A thin Feishu daemon use case that wires intake, dedupe, harness execution, and stream actions together.
32
- - In-memory and JSON file-backed Feishu CardKit target registries for binding agent runs to card element targets.
33
- - CardKit target preparation helpers that create a streaming card entity, reply to the source message, and bind `{ cardId, elementId }` before streaming starts.
34
- - A CardKit publisher port with per-run sequence handling, text-update coalescing, periodic flushing, and a simple 5 QPS rate-limit wrapper.
35
- - A fast-ack Feishu message queue and worker that separate event acceptance from agent execution and keep failed drains pending for retry.
36
- - A small Feishu agent runtime factory that assembles the harness, queue, worker, and event handlers.
37
- - A Feishu WebSocket daemon adapter that starts an official-SDK-shaped `WSClient` with the runtime handlers.
38
- - A background Feishu worker loop that drains accepted messages outside the WebSocket acknowledgement callback.
39
- - A small `rivus` CLI with conflict-safe project initialization, side-effect-free local readiness diagnostics, bootstrap loading, daemon startup, and `SIGINT`/`SIGTERM` shutdown.
40
- - A configured daemon bootstrap helper that wires Feishu WebSocket, CardKit target preparation, CardKit publishing, event logging, history replay, worker draining, and managed process lifecycle.
41
- - A daemon status reporter that combines active harness run details, process/transport/worker/queue state, and restored run/session history.
42
- - A small CardKit OpenAPI client for streaming text updates and replacing the card with completed, failed, or cancelled terminal projections.
43
- - Durable per-Endpoint Human Interaction services with JSONL snapshots, separate Tool Approval and User Decision state machines, allowlisted Feishu actors, expiring requests, and exact one-time Tool Broker consumption.
44
- - Feishu tenant access token provider, JSON fetch adapter, configured CardKit target preparation, configured CardKit publisher, and receive-message handler map for SDK integration.
45
- - Tests covering prompt execution, event subscription, durable event sinks, busy-run rejection, run/session state lookup, state snapshots, and loop failure events.
46
-
47
- Production SDK bootstrap files can stay thin by creating SDK clients and passing them into the configured bootstrap helper. See [examples/pi-feishu.bootstrap.ts](examples/pi-feishu.bootstrap.ts) for a concrete Pi SDK plus Feishu Node SDK long-connection bootstrap.
48
-
49
- ## Package Installation
50
-
51
- Published releases require Node 24 LTS and install from the public npm registry:
52
-
53
- ```bash
54
- npm install @rivus/agent
55
- npx rivus --help
56
- ```
57
-
58
- Create the default Personal Agent Home without cloning this repository or installing another local Rivus copy:
59
-
60
- ```bash
61
- npm install --global @rivus/agent
62
- rivus setup
63
- cp "$HOME/.rivus-agent/.env.example" "$HOME/.rivus-agent/.env"
64
- # Fill the Feishu and model-provider values in ~/.rivus-agent/.env.
65
- rivus doctor
66
- rivus start
67
- ```
68
-
69
- `setup` refuses to overwrite existing Home files. `start`, `status`, and `check-config` read `RIVUS_HOME` or `~/.rivus-agent`, use its configured Workspace as cwd, and keep state outside npm installation files. `doctor` checks the Home, global modules, manifest, Workspace, and enabled Endpoint credential references without importing Plugin code or contacting external services. Existing standalone projects remain supported through `rivus init <directory>` and `rivus doctor <directory>`. See the [local deployment runbook](docs/en/operations/local-deployment.md).
70
-
71
- The maintained Pi plus Feishu Bootstrap is the default Personal Home runtime, so its pinned Pi SDK and compatible
72
- Feishu SDK are installed transitively with `@rivus/agent`. Optional adapter SDKs such as ACP remain separate peers.
73
-
74
- ## Quick Start
75
-
76
- ```bash
77
- curl -fsSL https://vite.plus | bash
78
- vp install
79
- vp run check
80
- ```
81
-
82
- `vp run check` is the deterministic local and pull-request gate. Vite+ runs Oxfmt, type-aware Oxlint with TypeScript diagnostics, Vitest, and library packaging; the gate then packs and installs the npm archive in a clean temporary consumer, exercises both public exports and the CLI, and checks documentation and repository safety. High-severity production-dependency audit runs when production dependency inputs change, on a weekly workflow, and at publish/prepublish. Full CI and version preparation require explicit requests; ordinary pushes and PR metadata edits do not start heavy checks. Automatic Review provides repository/title feedback. Explicit CI always runs the complete gate for the current branch/head/base pair; see [Pull Request Gates](CONTRIBUTING.md#pull-request-gates) for dispatch and retry commands. Use `vp check --fix` for deterministic formatting and safe lint fixes, then review the diff before committing. The project runtime is pinned in `.node-version`.
83
-
84
- npm releases are produced only by the `Publish npm` GitHub Actions workflow after a GitHub Release is published for an exact `v<package version>` tag on `main`. The workflow uses npm Trusted Publishing through OIDC and contains no long-lived registry token. See the [npm release runbook](docs/en/operations/npm-release.md) for the one-time first-package bootstrap and normal release procedure.
85
-
86
- For a manifest-driven multi-agent deployment, keep `rivus.config.json` in Rivus Home and install declared Plugin packages beside the global Rivus package. `createConfiguredRivusDeploymentDaemon()` validates the manifest, resolves only declared modules from the Home or Host-trusted npm roots, disables deployments belonging to failed Optional Plugins, and blocks startup when a Required Plugin fails:
87
-
88
- ```json
89
- {
90
- "plugins": [
91
- {
92
- "id": "research-agent",
93
- "module": "research-agent/rivus-plugin",
94
- "required": true
95
- }
96
- ],
97
- "defaultAgentId": "researcher",
98
- "defaultEndpointId": "research-feishu",
99
- "agents": [
100
- {
101
- "agentId": "researcher",
102
- "pluginId": "research-agent",
103
- "profileId": "researcher",
104
- "endpointIds": ["research-feishu"],
105
- "memory": { "scopes": ["agent-private"], "tool": true },
106
- "tools": { "allow": ["research-agent/read"] }
107
- }
108
- ],
109
- "automations": [
110
- {
111
- "id": "daily-briefing",
112
- "agentId": "researcher",
113
- "templateId": "research-agent/daily-briefing",
114
- "enabled": true,
115
- "required": true,
116
- "schedule": "0 10 * * *",
117
- "timeZone": "Asia/Shanghai",
118
- "delivery": {
119
- "endpointId": "research-feishu",
120
- "targetRef": "env:DAILY_BRIEFING_TARGET",
121
- "targetType": "union_id"
122
- }
123
- }
124
- ],
125
- "backgroundSessions": {
126
- "enabled": false,
127
- "required": false,
128
- "stepTimeoutMs": 300000,
129
- "maxConcurrentSessions": 4,
130
- "leaseMs": 30000,
131
- "leaseRenewalIntervalMs": 10000,
132
- "maxConsecutiveFailures": 3,
133
- "retryBackoffMs": 30000,
134
- "sessionLifetimeMs": 86400000
135
- },
136
- "endpoints": [
137
- {
138
- "id": "research-feishu",
139
- "agentId": "researcher",
140
- "sessionNamespace": "research-feishu-v1",
141
- "credentialRef": "env:RESEARCH_FEISHU",
142
- "enabled": true,
143
- "required": true,
144
- "baseUrl": "https://open.feishu.cn",
145
- "streamMinIntervalMs": 200,
146
- "progressDisplay": "collapsed",
147
- "groupPolicy": "mention-only"
148
- }
149
- ]
150
- }
151
- ```
152
-
153
- The application Composition Root supplies Runtime, Endpoint, and Automation adapters:
154
-
155
- ```ts
156
- const daemon = await createConfiguredRivusDeploymentDaemon({
157
- manifestPath: "./rivus.config.json",
158
- createRuntime,
159
- createEndpoint,
160
- createAutomation
161
- });
162
-
163
- await daemon.start(); // rejects when an enabled Required Endpoint or Automation cannot start
164
- ```
165
-
166
- Endpoint factories receive a Host-bound `handle()` function. Inbound payload fields cannot select another Agent or Endpoint. `createConfiguredRivusDaemonBootstrap()` is the small, explicit single-lane Pi/Feishu composition; manifest deployments use the multi-agent path directly, without a parallel compatibility model.
167
-
168
- [`examples/rivus.config.json`](examples/rivus.config.json) and [`examples/pi-feishu-deployment.bootstrap.ts`](examples/pi-feishu-deployment.bootstrap.ts) form a concrete two-bot composition. Each `credentialRef: "env:<PREFIX>"` resolves only `<PREFIX>_APP_ID` and `<PREFIX>_APP_SECRET`; each Endpoint owns its WebSocket client, durable Inbox, bounded concurrent worker, CardKit target/delivery state, Human Interaction JSONL repository, and lifecycle. Its unique Manifest Session Namespace is prepended before the trusted Host route reaches a fair per-session Scheduler, Harness lane, and Pi Session Registry. Startup resolves the Bot Open ID, waits for the SDK `onReady` handshake, repairs missing terminal CardKit projections, and applies `groupPolicy` before the durable accept. The example restores a separate event log, Tool operation ledger, and Pi session directory per Agent Instance, plus one deployment-scoped append-only Agent Memory log. Native Pi tools default to none and are activated only by the resolved `runtimeToolGrantSet`; custom Tools come only from the resolved Agent definition. Extensions, prompt templates, themes, ambient project settings, and ambient Pi Skill discovery remain disabled. Pi loads existing user-global Skill roots plus only the selected Project Space's explicit sources, accepts an empty catalog, and validates every `/skill:<name>` before model execution. A bounded `AGENTS.md` View supplies context without granting authority.
169
-
170
- Interactive Endpoints default `progressDisplay` to `collapsed`: public assistant narration and inspectable Skill/Tool actions remain in chronological order, and every action owns a small native collapsible panel rather than being collected into one process appendix. `expanded` opens completed action panels initially and `hidden` omits them; a running action stays open so its safe parameters and current result are visible. A channel-neutral `RunPresentation` read model preserves stable steps, status, duration, and safe Tool input/result projections across streaming updates and successor cards. Sensitive fields are redacted, machine paths normalized, and long values explicitly truncated before a channel renderer sees them; private reasoning and raw Tool payloads never enter the presentation IR. Ordinary replies never gain an empty process panel. A rollover freezes the predecessor exactly as last rendered and starts a blank successor that receives only post-breakpoint content. Scheduled Automations use a separate final-only `AutomationPresentation` IR: Plugins own title, metadata, sections, items, notes, and sources, while the channel renderer owns visual components.
171
-
172
- An Endpoint may opt into Feishu's native process message with `"experimental": { "cotMessages": true }`. The deployment then requires a local `RIVUS_FEISHU_COT_BASE_URL`; no environment-specific origin is committed or inferred. Rivus creates the process message before the CardKit target so Feishu displays the process above the authoritative final answer, then renders redacted reasoning, model/Skill steps, and Tool calls into AG-UI COT events. Tool arguments remain valid redacted JSON as required by AG-UI, while the Feishu presenter uses narrow mappings for the deployment's known Tools, a sanitized bounded fallback for unknown Tools, and labeled result lines such as `查询天气 · 北京`. COT failures are reported and degrade to CardKit instead of failing the Agent Run; if CardKit preparation fails after COT creation, Rivus explicitly closes that process as failed.
173
-
174
- The example declares disabled `daily-ielts-word`, `a-share-pre-market`, and `a-share-post-market` Automations. Enable them only in a gitignored local manifest and set each `targetRef` variable to a Feishu receive ID. The scheduler accepts `M H * * *` and the weekday-only subset `M H * * 1-5` plus an IANA time zone. The A-share templates run at 09:00 and 16:00 on weekdays, request only the bounded evidence-backed briefing Tool, and relay its source-attributed facts and explicitly labeled inferences without letting the model invent market numbers or causal claims; see the [A-share briefing runbook](docs/en/operations/a-share-market-briefing.md). Each occurrence uses an Automation-only Agent Instance and Session, persists generated text before delivery, and sends with a stable Feishu UUID so restart or delivery retry does not create another model answer. An endpoint-less worker Agent may deliver through another enabled Agent's Host-owned Endpoint, allowing one visible Feishu bot to present multiple isolated task Profiles without inheriting their Tool grants or state. It is a local single-writer scheduler, not a distributed cron service.
175
-
176
- ```bash
177
- rivus --env-file .env.local \
178
- --bootstrap ./examples/pi-feishu-deployment.bootstrap.ts \
179
- --manifest ./examples/rivus.config.json
180
- ```
181
-
182
- The manifest deployment bootstrap enables Langfuse tracing when all three credential variables below are present. It exports one OpenTelemetry trace per Agent Run with nested Turn, model, and Tool spans. Content is metadata-only by default. Set `RIVUS_TELEMETRY_CONTENT=redacted` to include Run/Turn, authoritative per-model-call, and Tool input/output after recursive redaction and size bounding. Pi snapshots each model request from its effective system prompt, transcript, and active Tool schemas, plus the final assistant message; a transient application port sends those facts directly to the telemetry adapter without adding raw model content to the durable Agent event log. The built-in policy replaces known credential fields, common inline secrets, common personal identifiers, and machine home-directory prefixes. Model thinking, raw deltas, and raw error messages remain excluded. The heuristic remains defense in depth rather than a guarantee for arbitrary domain-specific secrets.
183
-
184
- ```bash
185
- LANGFUSE_BASE_URL=https://jp.cloud.langfuse.com
186
- LANGFUSE_PUBLIC_KEY=pk-lf-...
187
- LANGFUSE_SECRET_KEY=sk-lf-...
188
- RIVUS_TELEMETRY_ENVIRONMENT=local
189
- RIVUS_TELEMETRY_CONTENT=redacted
190
- ```
191
-
192
- Keep these values in `.env.local`. A partial Langfuse configuration fails at startup so an expected telemetry integration cannot silently drop traces. Export uses the regional Langfuse OTLP/HTTP endpoint and is batch-processed independently from the durable Agent event log; exporter failure does not fail an Agent Run. Daemon shutdown flushes and closes the exporter. `createTelemetryContentRedactor()` supplies the default bounded policy, and a custom `redactContent` function can be passed to `createOpenTelemetryAgentEventSink()` when a deployment needs stricter organization-specific rules.
193
-
194
- The shipped example also includes an Open-Meteo `current-weather` Tool. This one-shot command uses the deployment's default Agent without starting Feishu WebSockets, marks the local operator invocation explicitly, and produces a searchable `Agent 执行 -> Agent 处理 -> 模型调用 -> current-weather -> 模型调用` trace. Display names stay readable while standard `gen_ai.*` attributes preserve portable model, Tool, token, and cost semantics. Local CLI invocations have no human approval actor, so approval-required Tools remain unavailable.
195
-
196
- ```bash
197
- RIVUS_WEATHER_DEFAULT_LOCATION=上海 rivus \
198
- --env-file .env.local \
199
- --bootstrap ./examples/pi-feishu-deployment.bootstrap.ts \
200
- --manifest ./examples/rivus.config.json \
201
- --prompt "今天天气怎么样" \
202
- --session-key local:agent-a:weather-demo
203
- ```
204
-
205
- The same local prompt path can prove durable Agent Memory without starting Feishu. The Profile and Deployment must both grant `agent-private`, and the Deployment must set `memory.tool: true`. The CLI binds the Tool to `RIVUS_MEMORY_TENANT_ID`, `RIVUS_LOCAL_SUBJECT_ID`, and the resolved Agent; an optional `RIVUS_LOCAL_CONVERSATION_ID` supplies a separate trusted Conversation identity when that scope is granted. None of those identities are model arguments. Run two independent processes against the same state directory and use different Session Keys to prove cross-session Agent-private recall:
206
-
207
- ```bash
208
- RIVUS_DEPLOYMENT_STATE_DIR=.rivus/memory-demo \
209
- RIVUS_MEMORY_TENANT_ID=local-demo \
210
- RIVUS_LOCAL_SUBJECT_ID=operator \
211
- rivus --env-file .env.local \
212
- --bootstrap ./examples/pi-feishu-deployment.bootstrap.ts \
213
- --manifest ./examples/rivus.config.json \
214
- --prompt "请调用 rivus_memory 的 propose 命令记住:验收代号是 cedar-47。只报告工具结果。" \
215
- --session-key local:agent-a:memory-write
216
-
217
- RIVUS_DEPLOYMENT_STATE_DIR=.rivus/memory-demo \
218
- RIVUS_MEMORY_TENANT_ID=local-demo \
219
- RIVUS_LOCAL_SUBJECT_ID=operator \
220
- rivus --env-file .env.local \
221
- --bootstrap ./examples/pi-feishu-deployment.bootstrap.ts \
222
- --manifest ./examples/rivus.config.json \
223
- --prompt "请调用 rivus_memory 的 search 命令搜索 cedar-47,并原样告诉我找到的内容。" \
224
- --session-key local:agent-a:memory-read
225
- ```
226
-
227
- `propose` creates a `proposed` record; the model cannot call `confirm`, mark it conversation-safe, write arbitrary Shared User Profile text, or select another tenant, Agent, subject, conversation, audience, or storage path. `forget_request` may tombstone only an exact still-proposed private or Conversation record; confirmed and shared records require trusted control. Shared Profile search/read hides unconfirmed proposals. A trusted group audience removes Agent-private and Shared Profile scopes before Tool authority is issued, leaving only the current Conversation and confirmed Project scopes when granted. Replayed search/read operations recheck current visibility, so a tombstone cannot leak stale content from the Tool operation fence. The example stores Memory at `<RIVUS_DEPLOYMENT_STATE_DIR>/memory/agent-memory.jsonl` with owner-only file permissions and rejects a broken revision chain on restore.
228
-
229
- The separate [`examples/rivus-langfuse-demo.config.json`](examples/rivus-langfuse-demo.config.json) grants one dedicated profile a versioned HTML-publishing Skill and exactly two mutation Tools: write one operation-bound artifact and upload only that artifact to the current user's Feishu Drive root through `lark-cli`. The Runtime supplies one built-in `rivus_read_skill` reader only when the resolved Skill grant is non-empty; it is not a Plugin Tool and cannot read outside the immutable `skillGrantSet`. The normal two-bot manifest does not grant these Drive capabilities. Automation templates must request Skills explicitly and receive the exact intersection of Profile, Deployment, and Template grants. The uploader rejects arbitrary paths and symbolic links, then copies a no-follow-opened artifact into a private staging file before invoking `lark-cli`. Authenticate `lark-cli` as a user with Drive upload scope before running the demo:
230
-
231
- ```bash
232
- lark-cli auth status --json --verify
233
-
234
- rivus --env-file .env.local \
235
- --bootstrap ./examples/pi-feishu-deployment.bootstrap.ts \
236
- --manifest ./examples/rivus-langfuse-demo.config.json \
237
- --prompt "Read the granted Skill, create a polished Langfuse introduction, and upload it as langfuse-guide.html" \
238
- --session-key local:langfuse-demo:html-publish
239
- ```
240
-
241
- This produces a multi-step trace containing `技能 · Langfuse HTML publisher`, `write-html-artifact`, and `lark-drive-upload-html` between model generations. Durable Skill events record ID, version, digest, title, content length, and outcome without storing the Skill body. The Skill requires synthetic examples to be labeled and execution claims to come from supplied evidence or Tool results. The durable Tool operation ledger replays a completed upload result and fences an uncertain side effect instead of silently uploading twice. The publication boundary is recorded in [RFC 0009](rfcs/0009-skill-driven-html-drive-publication.md); the first-class Skill Runtime and E2E evidence contract are recorded in [RFC 0010](rfcs/0010-first-class-skill-runtime-and-cloud-e2e.md).
242
-
243
- Run the opt-in cloud E2E Harness after `lark-cli` user authentication and `.env.local` Langfuse configuration. It builds the package, uses a unique Session Key, file name, and content marker, runs the real Agent, inspects and downloads the returned Drive file, then polls Langfuse until the strict model/Skill/model/write/model/upload/model sequence is visible and the successful upload output identifies that same file. Every request is deadline-bounded, and only traces discovered for the unique Session are fetched. The Harness prints only sanitized evidence; it is intentionally not part of credential-free CI.
244
-
245
- ```bash
246
- npm run e2e:langfuse-drive
247
- ```
248
-
249
- The package also builds a `rivus` bin. The bin intentionally loads a bootstrap module instead of importing specific Pi or Feishu SDK packages in the core:
250
-
251
- ```bash
252
- FEISHU_APP_ID=cli_x \
253
- FEISHU_APP_SECRET=secret_x \
254
- rivus --bootstrap ./rivus.bootstrap.mjs
255
- ```
256
-
257
- The CLI can also own the manifest-driven startup chain. In this mode the Bootstrap exports `createRivusDeploymentAdapters(context)` with application-edge `createRuntime`, `createEndpoint`, and optional `createAutomation` factories; the CLI loads the Manifest, runs the Plugin Loader, constructs the Multi-agent Host, and then activates Endpoints followed by Automations:
258
-
259
- ```bash
260
- rivus --bootstrap ./rivus.bootstrap.mjs --manifest ./rivus.config.json
261
- ```
262
-
263
- `--status` reports the complete Deployment without activating Endpoints. `--prompt` runs the default Agent on the instance bound to `defaultEndpointId` without starting unrelated Endpoint transports. It supplies an explicit `local-cli` invocation origin, binds optional Memory identity from `RIVUS_MEMORY_TENANT_ID`, `RIVUS_LOCAL_SUBJECT_ID`, and `RIVUS_LOCAL_CONVERSATION_ID`, and allows granted non-approval Tools to run, while the empty actor set keeps approval-required Tools fail-closed. A Bootstrap may additionally expose a manifest replay adapter for the existing Feishu replay commands.
264
-
265
- `rivus --manifest ./rivus.config.json --check-config` performs side-effect-free schema and semantic validation, including unique identities, references, Session Namespaces, Automation delivery bindings, and the enabled default binding. It prints only the target reference name, never the resolved receive ID. It does not load the Bootstrap or Plugin code and does not require legacy `FEISHU_*` values.
266
-
267
- Manifest deployments may also expose an offline Recovery Control. Stop the daemon before using these commands: the bundled JSON/JSONL stores remain single-writer and do not provide cross-process locking. `--recovery-list` prints payload-free Dead Letter summaries and Tool operations currently fenced as `reconciliation-required`; raw worker and provider errors are replaced with bounded failure codes and generic operator guidance. Every mutation requires the exact listed revision plus an audit note, so a repeated or stale operator action fails closed.
268
-
269
- ```bash
270
- rivus --bootstrap ./rivus.bootstrap.mjs --manifest ./rivus.config.json --recovery-list
271
-
272
- install -m 600 /dev/null .rivus/recovery-note.txt
273
- ${EDITOR:-vi} .rivus/recovery-note.txt
274
-
275
- rivus --bootstrap ./rivus.bootstrap.mjs --manifest ./rivus.config.json \
276
- --requeue-dead-letter om_xxx \
277
- --endpoint-id feishu-agent-a \
278
- --expected-revision 5 \
279
- --recovery-note-file .rivus/recovery-note.txt
280
- ```
281
-
282
- Requeue resets the delivery's bounded attempt counter and retains the original payload behind the Inbox boundary. It is rejected while any pending or uncertain Tool operation remains bound to the same source message; reconcile those Tool fences first. An uncertain Tool operation must be reconciled from external evidence. Confirming `applied` requires the exact stable-JSON result that future retries should replay; confirming `not-applied` releases the existing fence so the same stable Operation ID may execute again.
283
-
284
- ```bash
285
- install -m 600 /dev/null .rivus/tool-result.json
286
- ${EDITOR:-vi} .rivus/tool-result.json
287
-
288
- rivus --bootstrap ./rivus.bootstrap.mjs --manifest ./rivus.config.json \
289
- --resolve-tool-operation operation_xxx \
290
- --instance-id instance_xxx \
291
- --expected-revision 2 \
292
- --tool-outcome applied \
293
- --tool-result-file .rivus/tool-result.json \
294
- --recovery-note-file .rivus/recovery-note.txt
295
- ```
296
-
297
- Recovery notes and Tool results are intentionally read from regular files with owner-only permissions (`0600` or stricter), not command arguments that can leak through shell history or process listings. Mutation output confirms identities, revision, actor, timestamp, and terminal state without echoing the note or applied result.
298
-
299
- If you already have an OpenClaw local config, you can print a Rivus-compatible env file instead of copying Feishu and GLM settings by hand:
300
-
301
- ```bash
302
- rivus --print-openclaw-env ./openclaw.json --pi-api-key-file /path/to/local-key.txt > .env.local
303
- ```
304
-
305
- The generated file includes Feishu credentials, the selected `PI_MODEL`, matching provider `PI_BASE_URL`, explicit OpenClaw thinking level as `PI_THINKING_LEVEL` when present, and `PI_API_KEY_FILE` when provided. Treat the output as secret-bearing local config and keep it in a gitignored file.
306
-
307
- Then point the CLI at that local file instead of exporting each variable by hand:
308
-
309
- ```bash
310
- rivus --env-file .env.local --bootstrap ./rivus.bootstrap.mjs
311
- ```
312
-
313
- To validate the local file and BYOK key path without loading a bootstrap module or printing secrets:
314
-
315
- ```bash
316
- rivus --env-file .env.local --check-config
317
- ```
318
-
319
- To print the bootstrap status as JSON without starting the long-connection daemon:
320
-
321
- ```bash
322
- rivus --env-file .env.local --bootstrap ./rivus.bootstrap.mjs --status
323
- ```
324
-
325
- To run one local prompt through the bootstrap harness without starting the long-connection daemon:
326
-
327
- ```bash
328
- rivus --env-file .env.local --bootstrap ./rivus.bootstrap.mjs --prompt "Say hello" --session-key local:main:cli
329
- ```
330
-
331
- To replay one Feishu receive-message payload through the bootstrap runtime without starting the long-connection daemon:
332
-
333
- ```bash
334
- rivus --env-file .env.local --bootstrap ./rivus.bootstrap.mjs --replay-feishu-event ./payload.json
335
- ```
336
-
337
- The replay JSON includes an `intake` section with `messageId`, `commandType`, `sessionKey`, and the tenant/chat/thread/agent reference that produced that key, so a captured Feishu payload can be checked for session mapping before live callbacks are debugged.
338
-
339
- To smoke test the same Feishu receive path without a captured payload file, synthesize a text message locally:
340
-
341
- ```bash
342
- rivus --env-file .env.local --bootstrap ./rivus.bootstrap.mjs --replay-feishu-text "local smoke" --feishu-message-id om_local_smoke
343
- ```
344
-
345
- The synthetic replay defaults to `oc_cli`, `omt_cli`, and `tenant_cli`, and accepts `--feishu-chat-id`, `--feishu-thread-id`, and `--feishu-tenant-key` when you need to exercise a specific Feishu session mapping. It disables Feishu run side effects for this one replay, so a fake message id does not create or reply with a CardKit card.
346
-
347
- The packaged example also supports live local status while the daemon is running:
348
-
349
- ```bash
350
- RIVUS_STATUS_PORT=17890 \
351
- rivus --env-file .env.local --bootstrap ./rivus.bootstrap.mjs
352
- ```
353
-
354
- From another terminal:
355
-
356
- ```bash
357
- rivus --status-url http://127.0.0.1:17890/status
358
- ```
359
-
360
- For a single-command inbound smoke check, start the daemon and wait until the Feishu callback or worker path is observed:
361
-
362
- ```bash
363
- rivus --env-file .env.local --bootstrap ./rivus.bootstrap.mjs --wait-receive handled --wait-timeout-ms 30000
364
- ```
365
-
366
- The bootstrap module should export `createRivusDaemonProcess(context)` or a default factory and return a started-ready daemon process. If the returned object also exposes `promptText(command)`, `rivus --prompt` uses that method for local model smoke tests without calling `start()`. If it exposes `replayReceiveMessage(payload)`, `rivus --replay-feishu-event <json>` reads the payload file, `rivus --replay-feishu-text <text>` builds a synthetic text payload, and both commands run the local receive-message replay path, print the replay result as JSON, and exit without calling `start()`. A typical module creates SDK clients at the edge, then delegates the local-daemon wiring and event-log hydration to `restoreConfiguredRivusDaemonBootstrap`:
367
-
368
- ```ts
369
- import { Effect } from "effect";
370
- import {
371
- createJsonFileFeishuCardTargetRegistry,
372
- createJsonFetchRequest,
373
- createJsonlAgentEventLog,
374
- restoreConfiguredRivusDaemonBootstrap
375
- } from "@rivus/agent";
376
-
377
- export async function createRivusDaemonProcess(context) {
378
- const bootstrap = await Effect.runPromise(
379
- restoreConfiguredRivusDaemonBootstrap({
380
- cardTargets: createJsonFileFeishuCardTargetRegistry({
381
- filePath: "./.rivus/feishu-card-targets.json"
382
- }),
383
- clock,
384
- config: context.config,
385
- eventDispatcher,
386
- eventLog: createJsonlAgentEventLog({
387
- filePath: "./.rivus/agent-events.jsonl"
388
- }),
389
- loop: piLoop,
390
- request: createJsonFetchRequest(),
391
- runIds,
392
- sleep,
393
- websocketClient
394
- })
395
- );
396
-
397
- return {
398
- running: () => bootstrap.process.running(),
399
- start: () => bootstrap.process.start(),
400
- status: () => bootstrap.status(),
401
- stop: () => bootstrap.process.stop()
402
- };
403
- }
404
- ```
405
-
406
- ## Configuration
407
-
408
- `loadRivusDaemonConfig(env)` parses a plain environment object and returns a typed config without reading `process.env` directly.
409
-
410
- Recognized variables:
411
-
412
- - `FEISHU_APP_ID` and `FEISHU_APP_SECRET` are required.
413
- - `FEISHU_BASE_URL` defaults to `https://open.feishu.cn`.
414
- - `FEISHU_STREAM_MIN_INTERVAL_MS` defaults to `200`, matching a conservative 5 QPS stream update envelope.
415
- - `RIVUS_AGENT_ID` defaults to `main`.
416
- - `PI_API_KEY` or `PI_API_KEY_FILE`, plus `PI_BASE_URL`, `PI_MODEL`, and `PI_THINKING_LEVEL`, are optional Pi adapter settings.
417
- - `PI_API_KEY_FILE` reads and trims a local key file; it is mutually exclusive with `PI_API_KEY`.
418
- - `PI_THINKING_LEVEL` accepts `off`, `minimal`, `low`, `medium`, `high`, or `xhigh`.
419
- - `LANGFUSE_BASE_URL`, `LANGFUSE_PUBLIC_KEY`, and `LANGFUSE_SECRET_KEY` optionally enable metadata-only OpenTelemetry export in the manifest deployment bootstrap and must be set together.
420
- - `RIVUS_TELEMETRY_CONTENT` defaults to `metadata-only`; set it explicitly to `redacted` to export recursively redacted and size-bounded Run/Turn/Model/Tool content. Other values are rejected.
421
- - `RIVUS_WEATHER_DEFAULT_LOCATION` selects the example `current-weather` Tool's fallback city and defaults to `北京`; it is not a Core setting.
422
- - `RIVUS_TELEMETRY_ENVIRONMENT` defaults to `NODE_ENV`, then `local`; `RIVUS_TELEMETRY_SERVICE_NAME` defaults to `rivus-agent`.
423
-
424
- For both SDK bootstrap templates, `PI_MODEL` should use Pi's provider/model form when `PI_API_KEY`, `PI_API_KEY_FILE`, or `PI_BASE_URL` is set, for example `anthropic/claude-opus-4-5` or `zai/glm-5.2` for GLM BYOK. The templates create Pi's isolated `ModelRuntime`, apply the resolved API key with `setRuntimeApiKey()`, resolve the model with `getModel(provider, modelId)`, and apply `PI_BASE_URL` by merging a local `.rivus/pi-models.json` provider override without removing existing custom models or other providers, matching Pi's `models.json` configuration model.
425
-
426
- ## Core Usage
427
-
428
- ### External Agent Plugin definitions
429
-
430
- External Agent packages can register declarative Profiles, Tools, Skills, and Automation templates without starting runtime resources. A Host-owned Deployment then narrows the Profile's requested Tools with an exact allowlist:
431
-
432
- ```ts
433
- import {
434
- createRivusPluginCatalog,
435
- resolveRivusAgentDefinition,
436
- RIVUS_PLUGIN_API_VERSION
437
- } from "@rivus/agent";
438
-
439
- const catalog = createRivusPluginCatalog();
440
- catalog.registerPlugin({
441
- manifest: {
442
- apiVersion: RIVUS_PLUGIN_API_VERSION,
443
- id: "example-agent",
444
- version: "1.0.0"
445
- },
446
- register(registry) {
447
- registry.registerTool({
448
- createExecutor: () => ({ execute: async () => ({ ok: true }) }),
449
- description: "Read an example source",
450
- digest: "sha256:example-read-v1",
451
- id: "example-agent/read",
452
- idempotency: "supported",
453
- inputSchema: { type: "object" },
454
- risk: "observe",
455
- version: "1.0.0"
456
- });
457
- registry.registerAgentProfile({
458
- displayName: "Example Agent",
459
- id: "example",
460
- memory: { scopes: [] },
461
- model: { provider: "example" },
462
- runtimeTools: { allow: ["read", "grep", "find", "ls"] },
463
- skills: { allow: [] },
464
- systemPrompt: "Use trusted example sources.",
465
- tools: { allow: ["example-agent/read"] }
466
- });
467
- }
468
- });
469
-
470
- const definition = resolveRivusAgentDefinition(catalog, {
471
- agentId: "example",
472
- endpointIds: ["example-feishu"],
473
- pluginId: "example-agent",
474
- profileId: "example",
475
- runtimeTools: { allow: ["read", "grep"] },
476
- skills: { allow: [] },
477
- tools: { allow: ["example-agent/read"] }
478
- });
479
- ```
480
-
481
- The resolved definition exposes immutable Tool schemas, a `ToolGrantSet`, and a `runtimeToolGrantSet` containing only `read` and `grep`; executor factories remain inside the Catalog boundary. Wildcards, duplicate identifiers, unknown references, and incompatible Plugin API versions are rejected. Missing Runtime Tool allowlists resolve to an empty native grant.
482
-
483
- External Plugin packages can run reusable conformance checks from the testing subpath:
484
-
485
- ```ts
486
- import { assertRivusPluginConforms } from "@rivus/agent/testing";
487
-
488
- await assertRivusPluginConforms({ plugin, deployment });
489
- ```
490
-
491
- ### Agent Harness
492
-
493
- ```ts
494
- import { Effect, Stream } from "effect";
495
- import {
496
- createAgentHarnessClient,
497
- createAgentRuntime,
498
- createAgentDomainEventHandler,
499
- createAgentDomainEventSinkFromCallback,
500
- createAgentRunUpdateHandler,
501
- createDefaultAgentHarnessClientFromTextCallback,
502
- createDefaultAgentHarnessFromTextCallback,
503
- createDefaultAgentRuntimeFromTextCallback,
504
- createJsonlAgentEventLog,
505
- isAssistantTextDeltaEvent,
506
- restoreAgentHistory
507
- } from "@rivus/agent";
508
-
509
- const eventLog = createJsonlAgentEventLog({
510
- filePath: "./.rivus/agent-events.jsonl"
511
- });
512
-
513
- const auditSink = createAgentDomainEventSinkFromCallback((event) => {
514
- console.log(event.type);
515
- });
516
-
517
- const harness = createDefaultAgentHarnessFromTextCallback(
518
- (input) => `Hello ${input.text}`,
519
- {
520
- eventSinks: [eventLog, auditSink]
521
- }
522
- );
523
-
524
- harness.subscribe((event) => {
525
- if (isAssistantTextDeltaEvent(event)) {
526
- console.log(event.delta);
527
- }
528
- console.log(event.type);
529
- });
530
-
531
- const finalText = await Effect.runPromise(
532
- harness.promptText({
533
- sessionKey: "feishu:tenant:chat:thread",
534
- text: "Say hello"
535
- })
536
- );
537
-
538
- console.log(finalText);
539
-
540
- const runtime = createAgentRuntime(harness, {
541
- mainSessionKey: "feishu:tenant:chat:thread"
542
- });
543
- console.log(await runtime.promptText("Say hello from the main session"));
544
- console.log((await runtime.promptTurn("Return a turn")).assistantText);
545
-
546
- const chat = harness.forSession("feishu:tenant:chat:thread");
547
- const unsubscribeChat = chat.subscribe((event) => {
548
- console.log(event.type);
549
- });
550
- const unsubscribeChatUpdates = chat.subscribeUpdates(({ state }) => {
551
- console.log(state.phase, state.finalText);
552
- });
553
- const unsubscribeRunUpdates = chat.subscribeRunUpdates("run_123", ({ state }) => {
554
- console.log(state.phase);
555
- });
556
- const chatText = await Effect.runPromise(chat.promptText("Say hello"));
557
- const chatTurn = await Effect.runPromise(chat.promptTurn("Say hello"));
558
- const chatPromptSnapshot = await Effect.runPromise(chat.promptSnapshot("Say hello"));
559
- const chatRunSnapshot = await Effect.runPromise(chat.promptRunSnapshot("Say hello"));
560
- const chatResult = await Effect.runPromise(chat.prompt("Say hello"));
561
- const chatSnapshot = chat.getSnapshot();
562
- console.log(chatSnapshot.availability.busy, chatSnapshot.state?.finalText);
563
- console.log(chatTurn.assistantText);
564
- console.log(chatPromptSnapshot.summary?.finalText);
565
- console.log(chatRunSnapshot.summary.finalText);
566
- console.log(chatResult.snapshot.state?.finalText);
567
- console.log(chatResult.runSnapshot.summary.finalText);
568
- console.log(chat.getRunSnapshot(chatResult.runId)?.summary.finalText);
569
- await Effect.runPromise(
570
- chat.promptWithEvents(
571
- "Say hello",
572
- createAgentDomainEventHandler((event) => {
573
- console.log(event.type);
574
- })
575
- )
576
- );
577
- await Effect.runPromise(
578
- chat.promptWithUpdates(
579
- "Say hello",
580
- createAgentRunUpdateHandler(async ({ state }) => {
581
- await Promise.resolve();
582
- console.log(state.phase, state.finalText);
583
- })
584
- )
585
- );
586
- await Effect.runPromise(
587
- Stream.runForEach(chat.stream("Say hello"), (event) =>
588
- Effect.sync(() => {
589
- console.log(event.type);
590
- })
591
- )
592
- );
593
- await Effect.runPromise(
594
- Stream.runForEach(chat.streamText("Say hello"), (delta) =>
595
- Effect.sync(() => {
596
- console.log(delta);
597
- })
598
- )
599
- );
600
- await Effect.runPromise(
601
- Stream.runForEach(chat.streamUpdates("Say hello"), ({ state }) =>
602
- Effect.sync(() => {
603
- console.log(state.phase, state.finalText);
604
- })
605
- )
606
- );
607
- await Effect.runPromise(chat.cancelRun("run_from_card", "user cancelled"));
608
-
609
- console.log(chat.sessionKey);
610
- console.log(chatText);
611
- console.log(chatResult.updates.at(-1)?.state.phase);
612
- console.log(chat.getAvailability().busy);
613
- console.log(chat.getActiveRun()?.runId);
614
- console.log(chat.getActiveRunState()?.phase);
615
- console.log(chat.getHistory().totalEvents);
616
- console.log(chat.getSummary()?.runCount);
617
- console.log(chat.getRuns().map((run) => run.runId));
618
- console.log(chat.getLatestRunSummary()?.runId);
619
- console.log(chat.getLatestRunSnapshot()?.summary.finalText);
620
- console.log(chat.getRunSummary(chatResult.runId)?.prompt);
621
- console.log(chat.getRunEvents(chatResult.runId)?.length);
622
- console.log(chat.getRunUpdates(chatResult.runId)?.at(-1)?.state.phase);
623
- console.log(chat.getRunState(chatResult.runId)?.phase);
624
- console.log(chat.getState()?.phase);
625
-
626
- const client = createAgentHarnessClient(harness);
627
- const chatClient = client.forSession("feishu:tenant:chat:thread");
628
- const promiseText = await chatClient.promptText("Say hello from a Promise adapter");
629
- const promiseTurn = await chatClient.promptTurn("Say hello from a Promise adapter");
630
- const promiseAttempt = await chatClient.tryPromptText("Try without throwing");
631
- if (promiseAttempt.ok) {
632
- console.log(promiseAttempt.value);
633
- } else {
634
- console.log(promiseAttempt.error);
635
- }
636
- await chatClient.promptWithUpdates("Stream progress", async ({ state }) => {
637
- console.log(state.phase, state.finalText);
638
- });
639
- await chatClient.promptWithText("Stream text with final result", async (delta) => {
640
- console.log(delta);
641
- });
642
- for await (const delta of chatClient.streamText("Stream text")) {
643
- console.log(delta);
644
- }
645
- console.log(promiseText);
646
- console.log(promiseTurn.assistantText);
647
-
648
- const quickClient = createDefaultAgentHarnessClientFromTextCallback((input) => `Hello ${input.text}`);
649
- console.log(await quickClient.forSession("feishu:tenant:chat:thread").promptText("Say hello"));
650
-
651
- const quickRuntime = createDefaultAgentRuntimeFromTextCallback((input) => `Hello ${input.text}`, undefined, {
652
- mainSessionKey: "feishu:tenant:chat:thread"
653
- });
654
- console.log(await quickRuntime.promptText("Say hello from the runtime"));
655
- console.log(await quickRuntime.mainClient.promptText("Say hello from a Promise adapter"));
656
- console.log(await Effect.runPromise(quickRuntime.mainSession.promptText("Say hello from Effect")));
657
-
658
- unsubscribeChat();
659
- unsubscribeChatUpdates();
660
- unsubscribeRunUpdates();
661
-
662
- const result = await Effect.runPromise(
663
- harness.prompt({
664
- sessionKey: "feishu:tenant:chat:thread",
665
- text: "Say hello"
666
- })
667
- );
668
-
669
- console.log(result.finalText);
670
- console.log(result.updates.at(-1)?.state.phase);
671
- console.log(harness.getActiveRun()?.sessionKey);
672
- console.log(harness.getActiveRunId());
673
- console.log(harness.getHistory().totalEvents);
674
- console.log(harness.getRunEvents(result.runId)?.map((event) => event.type));
675
- console.log(harness.getRunUpdates(result.runId)?.map((update) => update.state.phase));
676
- console.log(harness.getRunSummary(result.runId)?.toolExecutionCount);
677
- console.log(harness.getRunState(result.runId)?.phase);
678
- console.log(harness.getSessionHistory("feishu:tenant:chat:thread").totalEvents);
679
- console.log(harness.getSessionSummary("feishu:tenant:chat:thread")?.latestRunId);
680
- console.log(harness.getSessionRuns("feishu:tenant:chat:thread").map((run) => run.runId));
681
- console.log(harness.getSessionLatestRunSummary("feishu:tenant:chat:thread")?.runId);
682
- console.log(harness.getSessionLatestRunSnapshot("feishu:tenant:chat:thread")?.summary.finalText);
683
- console.log(harness.getSessionState("feishu:tenant:chat:thread")?.finalText);
684
-
685
- const streamedResult = await Effect.runPromise(
686
- harness.promptWithEvents(
687
- {
688
- sessionKey: "feishu:tenant:chat:thread",
689
- text: "Say hello"
690
- },
691
- createAgentDomainEventHandler((event) => {
692
- console.log(event.type);
693
- console.log(harness.getActiveRunState()?.finalText);
694
- })
695
- )
696
- );
697
-
698
- console.log(streamedResult.finalText);
699
-
700
- await Effect.runPromise(
701
- harness.promptWithUpdates(
702
- {
703
- sessionKey: "feishu:tenant:chat:thread",
704
- text: "Say hello"
705
- },
706
- createAgentRunUpdateHandler(({ event, state }) => {
707
- console.log(event.type, state.phase, state.finalText);
708
- })
709
- )
710
- );
711
-
712
- await Effect.runPromise(
713
- Stream.runForEach(
714
- harness.stream({
715
- sessionKey: "feishu:tenant:chat:thread",
716
- text: "Say hello"
717
- }),
718
- (event) =>
719
- Effect.sync(() => {
720
- console.log(event.type);
721
- })
722
- )
723
- );
724
-
725
- await Effect.runPromise(
726
- Stream.runForEach(
727
- harness.streamText({
728
- sessionKey: "feishu:tenant:chat:thread",
729
- text: "Say hello"
730
- }),
731
- (delta) =>
732
- Effect.sync(() => {
733
- console.log(delta);
734
- })
735
- )
736
- );
737
-
738
- await Effect.runPromise(
739
- Stream.runForEach(
740
- harness.streamUpdates({
741
- sessionKey: "feishu:tenant:chat:thread",
742
- text: "Say hello"
743
- }),
744
- ({ event, state }) =>
745
- Effect.sync(() => {
746
- console.log(event.type, state.phase, state.finalText);
747
- })
748
- )
749
- );
750
- ```
751
-
752
- `createAgentHarnessClient(harness)` is an optional Promise facade for non-Effect adapters. The core harness remains Effect-first, while the client exposes Promise-returning prompt, callback, async-iterable stream, and cancellation helpers plus the same synchronous read-model and subscription methods. `promptWithText()` filters ordered domain events to assistant text deltas while still returning the final prompt result, which keeps IM progress renderers out of event-type plumbing. `tryPrompt()` and `tryPromptText()` return `{ ok: true, value }` or `{ ok: false, error }`, so non-Effect adapters can branch on harness failures without exception control flow. `createDefaultAgentHarnessClient()`, `createDefaultAgentHarnessClientFromCallback()`, and `createDefaultAgentHarnessClientFromTextCallback()` compose the same facade with default clock/run-id wiring for one-step local bootstraps. `createAgentRuntime(harness)` wraps any existing harness in a tiny session-first facade: call `runtime.promptText()`, `runtime.promptTurn()`, or `runtime.tryPromptText()` for the main session directly, use `mainClient` when an application wants the full Promise session client, use `mainSession` when it wants Effect helpers, and use `harness` when it needs the full lower-level use case. `createDefaultAgentRuntime()`, `createDefaultAgentRuntimeFromCallback()`, and `createDefaultAgentRuntimeFromTextCallback()` compose that same facade with default harness wiring.
753
-
754
- Use `promptTurn()` when an adapter wants the completed user/assistant turn directly. It follows the same prompt path as `promptText()` and `prompt()`, and returns the replayed `AgentTranscriptTurn` with user text, assistant text, thinking text, phase, timing, and tool counts.
755
-
756
- `createDefaultAgentHarness` wires a system clock and UUID run ids by default; pass `clock` or `runIds` when deterministic tests or custom runtime boundaries need them. `createDefaultAgentHarnessFromCallback(run, options)` adds the same default runtime wiring while accepting a callback-shaped event source directly; `createDefaultAgentHarnessFromTextCallback(generate, options)` is the shortest bootstrap when a local SDK or model call only returns final assistant text. Pass `initialEvents` when a process has restored append-only domain events and wants the new harness to seed run-event timelines, process-local history snapshots, transcript snapshots, and same-session continuation before accepting work; pass `initialRunStates` when only projected run/session indexes are available. `createFeishuAgentRuntime` forwards both options to its internal harness. Restored non-terminal states remain available through `getRunState(runId)` but are not used as same-session continuation state, so a crash-time partial run is not treated as the previous answer. `AgentLoopInput.previousSessionState` gives loop adapters the latest terminal projected `AgentRunState` for the same `sessionKey` before the new run is accepted, `AgentLoopInput.previousSessionTranscript` gives them the already completed same-session turns before the current prompt, and `AgentLoopInput.messages` gives stateless SDK/RPC adapters provider-friendly `{ role, content }` conversation messages derived from that transcript plus the current user prompt; adapters do not need to query the harness for those views. `AgentLoopInput.abortSignal` is aborted when a harness cancellation request succeeds, so SDK and RPC adapters can stop underlying work in addition to letting the Effect stream unwind. `forSession(sessionKey)` returns a small `AgentSessionHandle` that binds `promptText(text)`, `promptSnapshot(text)`, `promptRunSnapshot(text)`, `prompt(text)`, `promptWithEvents(text, onEvent)`, `promptWithUpdates(text, onUpdate)`, `stream(text)`, `streamUpdates(text)`, `cancelActiveRun(reason)`, `cancelRun(runId, reason)`, best-effort event/update subscription, run-specific event/update subscription, availability lookup, active-run lookup, active-state lookup, history lookup, transcript lookup, session-summary lookup, run-list lookup, latest-run-summary lookup, latest-run-snapshot lookup, run-summary lookup, run-snapshot lookup, run-event lookup, run-update lookup, run-state lookup, session-state lookup, and a combined session snapshot to one stable session key, which is useful for IM adapters that should not repeatedly rebuild the same key. `promptText()` is the shortest path for callers that only need final assistant text while still using the same domain events, sinks, busy gate, and error model as `prompt()`. `promptSnapshot()` runs the same prompt path but returns only the completed `AgentSessionSnapshot`, which is useful when an adapter wants the final session read model instead of event/update history. `promptRunSnapshot()` runs the same prompt path but returns only the completed `AgentRunSnapshot`, which is useful when an adapter wants one run's detail read model instead of the session aggregate. `prompt()` returns accepted domain events, ordered `AgentRunUpdate` values, the completed session snapshot, and the completed run snapshot, so result-only callers can inspect final read models and event/state projection timeline without registering a handler or re-querying the harness. `promptWithEvents()` is the adapter-friendly path when a caller needs ordered domain-event side effects and the final `AgentPromptResult` from the same run. `promptWithUpdates()` carries each ordered domain event together with the prior `AgentRunState` when one exists and the state after that event has been projected, so adapters that render progress do not need to re-read harness state or keep local previous-frame bookkeeping. `createAgentDomainEventHandler()` and `createAgentRunUpdateHandler()` adapt sync or async callbacks into those ordered prompt handlers without changing the harness failure model. `stream()` exposes the ordered domain-event sequence as an Effect stream, and `streamUpdates()` exposes the same event-plus-state sequence for adapters that prefer stream composition over callbacks; interrupting either stream aborts the loop input signal, records an internal `agent_run_cancelled` event for sinks, subscribers, and read models, and releases the active run without emitting that terminal event to the interrupted consumer. `assistant_thinking_delta` events are projected into `AgentRunState.thinkingText` separately from `finalText`; Feishu stream projection still publishes only assistant text by default. `eventSinks` are awaited before the harness updates in-memory state or notifies subscribers; use `createAgentDomainEventSink()` for Effect-backed sinks or `createAgentDomainEventSinkFromCallback()` for sync or async callback sinks when a reliable side effect should share that sink contract. If a sink fails, including a synchronous `append()` throw before an Effect is returned, the run fails with `AgentEventSinkFailed` and no partially exposed event is published. Loop failures, including synchronous `AgentLoop.run()` throws before a stream is returned, are reported as `AgentLoopFailed` with the failed `AgentRunState`, including any partial text already emitted. Ordered event handler failures are reported as `AgentEventHandlerFailed` with the current `AgentRunState` after the triggering event has been projected. Busy rejections are reported as `AgentHarnessBusy` with the active `{ runId, sessionKey }` plus `activeState` when a projection is already available, so adapters can render the current phase and partial text directly. `cancelActiveRun(reason)` requests cancellation for the current busy run, while `cancelRun(runId, reason)` cancels only when the supplied run id still matches the active run; `forSession(sessionKey).cancelActiveRun(reason)` additionally requires the active run to belong to that session, and `forSession(sessionKey).cancelRun(runId, reason)` requires both the session and exact run id to match. Successful cancellation paths record `agent_run_cancelled`, project the run to `cancelled`, and fail the caller with `AgentRunCancelled` carrying partial state. Use exact run-id cancellation for stale-prone external UI or card callbacks that may arrive after a newer run starts; use session-active cancellation only for trusted same-session controls that intentionally cancel the currently visible run. `subscribe()` and `subscribeUpdates()` callbacks are best-effort observers; thrown listener errors do not fail runs, so reliable side effects should use `eventSinks`. `subscribeRun(runId)` and `subscribeRunUpdates(runId)` apply the same observer semantics while forwarding only one run; `forSession(sessionKey).subscribe()`, `subscribeUpdates()`, `subscribeRun(runId)`, and `subscribeRunUpdates(runId)` additionally require the projected run state to belong to the bound session, including text/tool/terminal events that do not carry `sessionKey` themselves. `getAvailability()` exposes a single idle/busy snapshot for adapters that want to render or reject work without probing by starting another prompt. `forSession(sessionKey).getAvailability()` returns the same idle state when the harness is idle, includes active details only when the active run belongs to that session, and otherwise returns `busy: true` with `activeInSession: false`. `getActiveRun()` exposes the current busy `{ runId, sessionKey }`, `getActiveRunId()` remains as a convenience for run-id-only callers, and `getActiveRunState()` exposes the in-flight `AgentRunState` only while a run is active. `forSession(sessionKey).getActiveRun()` and `forSession(sessionKey).getActiveRunState()` return those active projections only when the active run belongs to that session. `getHistory()`, `getSessionHistory(sessionKey)`, `getSessionTranscript(sessionKey)`, `getSessionSnapshot(sessionKey)`, `getSessionSummary(sessionKey)`, `getSessionRuns(sessionKey)`, `getSessionLatestRunSummary(sessionKey)`, `getSessionLatestRunSnapshot(sessionKey)`, `getRunSnapshot(runId)`, `getRunSummary(runId)`, `getRunEvents(runId)`, `getRunUpdates(runId)`, and `getRunState(runId)` expose process-local read-model copies for adapters while the process is alive; `getSessionSnapshot(sessionKey)` returns one session's availability, latest state, aggregate summary, run list, latest run summary, and transcript in one session-safe object; `getSessionTranscript(sessionKey)` returns user/assistant turns replayed from domain events for one external conversation, `createAgentTranscriptMessages(transcript)` formats completed turns as `{ role, content }` messages, `createAgentConversationMessages({ transcript, text })` appends the current user prompt to those messages, `getSessionSummary(sessionKey)` returns the aggregate run count, latest run id, phase, text, thinking, and tool counts for one external conversation, `getSessionRuns(sessionKey)` returns just the session-scoped run summaries for list/detail UIs, `getSessionLatestRunSummary(sessionKey)` returns the latest run summary for status surfaces, `getSessionLatestRunSnapshot(sessionKey)` returns the latest run detail snapshot for session-scoped detail surfaces, `getRunSnapshot(runId)` returns one run's events, ordered update timeline, latest state, and summary for detail surfaces, `getRunSummary(runId)` returns one run's prompt, timing, text, thinking, and tool-count summary, and `getRunUpdates(runId)` replays the same event/state update timeline shape returned by `prompt()`. `getSessionState(sessionKey)` exposes the latest session projection, and `forSession(sessionKey).getHistory()`, `getTranscript()`, `getSnapshot()`, `getSummary()`, `getRuns()`, `getLatestRunSummary()`, `getLatestRunSnapshot()`, `getRunSnapshot(runId)`, `getRunSummary(runId)`, `getRunEvents(runId)`, `getRunUpdates(runId)`, and `getRunState(runId)` return data only for the bound session. `eventLog.readRun(runId)` returns persisted events that can be replayed with `evolveAgentRun`.
757
-
758
- `streamText(command)` and session-bound `streamText(text)` expose only ordered assistant text deltas while reusing the same prompt path, sink ordering, busy gate, cancellation behavior, and state projection as `stream()`. `assistant_thinking_delta` still updates `AgentRunState.thinkingText`, but text-only stream consumers receive only assistant text.
759
-
760
- Adapters that can expose model/runtime events through a callback can stay thin with `createAgentLoopFromCallback`:
761
-
762
- ```ts
763
- import { createAgentLoopFromCallback } from "@rivus/agent";
764
-
765
- const loop = createAgentLoopFromCallback(async function* (input) {
766
- for await (const event of externalRuntime.run(input.text, { signal: input.abortSignal })) {
767
- yield event;
768
- }
769
- });
770
- ```
771
-
772
- When an adapter only has assistant text chunks, it can return or yield strings; the loop normalizes them to `assistant_text_delta` events before the harness sees them.
773
-
774
- Effect-native adapters can return an Effect from the same helper:
775
-
776
- ```ts
777
- import { Effect } from "effect";
778
- import { createAgentLoopFromCallback } from "@rivus/agent";
779
-
780
- const loop = createAgentLoopFromCallback((input) =>
781
- Effect.succeed([`Hello ${input.text}`])
782
- );
783
- ```
784
-
785
- They can also return an Effect `Stream` directly:
786
-
787
- ```ts
788
- import { Stream } from "effect";
789
- import { createAgentLoopFromCallback } from "@rivus/agent";
790
-
791
- const loop = createAgentLoopFromCallback((input) =>
792
- Stream.fromIterable([`Hello ${input.text}`])
793
- );
794
- ```
795
-
796
- Adapters that already receive an async sequence of model/runtime events can also use `createAsyncIterableAgentLoop`:
797
-
798
- ```ts
799
- import { createAsyncIterableAgentLoop } from "@rivus/agent";
800
-
801
- const loop = createAsyncIterableAgentLoop({
802
- run: async function* (input) {
803
- for await (const event of externalRuntime.run(input.text)) {
804
- yield event;
805
- }
806
- }
807
- });
808
- ```
809
-
810
- Simple demos or SDK calls that return final text can use `createTextAgentLoopFromCallback`:
811
-
812
- ```ts
813
- import { createTextAgentLoopFromCallback } from "@rivus/agent";
814
-
815
- const loop = createTextAgentLoopFromCallback(async (input) => {
816
- return externalModel.generate(input.text);
817
- });
818
- ```
819
-
820
- Effect-native text generators can use `createTextAgentLoop`:
821
-
822
- ```ts
823
- import { Effect } from "effect";
824
- import { createTextAgentLoop } from "@rivus/agent";
825
-
826
- const loop = createTextAgentLoop({
827
- generate: (input) => Effect.succeed(`Hello ${input.text}`)
828
- });
829
- ```
830
-
831
- Local demos and unit tests with a known sync or async script can use `createEventAgentLoop`:
832
-
833
- ```ts
834
- import { createAgentLoopThinkingDelta, createEventAgentLoop } from "@rivus/agent";
835
-
836
- const loop = createEventAgentLoop({
837
- events: [
838
- createAgentLoopThinkingDelta("Thinking..."),
839
- "Hello"
840
- ]
841
- });
842
-
843
- const inputDrivenLoop = createEventAgentLoop({
844
- events: (input) => [`Hello ${input.text}`]
845
- });
846
-
847
- const asyncInputDrivenLoop = createEventAgentLoop({
848
- events: async (input) => [`Hello ${input.text}`]
849
- });
850
- ```
851
-
852
- At daemon startup, `restoreAgentHistory(eventLog)` reads all persisted domain events and returns run/session summaries for adapters or CLI status views. Run and session summaries expose `finalText`, `thinkingText`, `toolExecutionCount`, and `failedToolExecutionCount`, so status surfaces can render common projections without replaying state themselves:
853
-
854
- ```ts
855
- const history = await Effect.runPromise(restoreAgentHistory(eventLog));
856
-
857
- console.log(history.sessions[0]?.latestRunId);
858
- ```
859
-
860
- `restoreConfiguredRivusDaemonBootstrap()` reads the same event log into `AgentHarness.initialEvents`, so `bootstrap.runtime.harness.getHistory()`, `getSessionHistory(sessionKey)`, `getRunEvents(runId)`, and `getRunUpdates(runId)` include restored events before the worker accepts new Feishu messages.
861
-
862
- The configured bootstrap also exposes `status()` for daemon-facing health and status surfaces:
863
-
864
- ```ts
865
- const status = await Effect.runPromise(bootstrap.status());
866
-
867
- console.log(status.harness.busy, status.harness.activeRunState?.finalText, status.queue.pending);
868
- ```
869
-
870
- The CLI `--status` path uses that same method when the bootstrap returns a daemon with `status()`. It does not start the WebSocket client or worker loop, so it is safe for local inspection of persisted history and bootstrap-visible state. Live status is derived from `AgentHarness.getAvailability()` and includes the active run state when a run is currently draining, including partial text and tool execution projection.
871
-
872
- The CLI replay paths also avoid starting long-running transports. `--replay-feishu-event <json>` is for captured Feishu payloads, while `--replay-feishu-text <text>` creates a local `im.message.receive_v1`-shaped text payload and calls `replayReceiveMessage(payload, { sideEffects: "disabled" })`. Unlike `--prompt`, the synthetic replay still resolves the Feishu `sessionKey` from tenant/chat/thread/agent fields, which makes it useful for checking OpenClaw-style session mapping before a real callback is available without replying to a fake Feishu message id.
873
-
874
- For live status from an already-running daemon, inject `createRivusDaemonStatusHttpServer` through `createStatusTransport` on `createConfiguredRivusDaemonBootstrap`. The example bootstrap wires this when `RIVUS_STATUS_PORT` is set, and `rivus --status-url <url>` prints that live status JSON without loading a bootstrap module or requiring Feishu credentials. Configured status includes `receive.lastAccepted` with the latest raw Feishu message id/chat/thread/tenant seen on the fast-ack path, and `receive.lastHandled` with the latest worker-handled message plus resolved `intake.sessionKey`. Both receive observations include `observedAt`, rendered as an ISO timestamp in JSON, so timeout diagnostics can distinguish fresh callbacks from stale state.
875
-
876
- During live Feishu E2E checks, `--wait-receive accepted` waits until the long-connection callback has reached the fast-ack path, while `--wait-receive handled` waits until the worker has resolved the session and completed handling. Use it with `--status-url` when the daemon is already running, or with `--bootstrap` to start the daemon, wait, print the final status JSON, and stop it automatically:
877
-
878
- ```bash
879
- rivus --status-url http://127.0.0.1:17890/status --wait-receive handled --wait-timeout-ms 30000
880
- rivus --env-file .env.local --bootstrap ./rivus.bootstrap.mjs --wait-receive handled --wait-timeout-ms 30000
881
- ```
882
-
883
- To avoid accepting an older handled observation during live E2E, send a Feishu message with a unique marker and wait for that marker in `receive.lastHandled.intake.text`:
884
-
885
- ```bash
886
- rivus --env-file .env.local --bootstrap ./rivus.bootstrap.mjs --wait-receive handled --wait-receive-text rivus-live-marker-123 --wait-timeout-ms 30000
887
- ```
888
-
889
- If the sender API returns a Feishu `message_id`, wait for that exact callback instead of matching only text:
890
-
891
- ```bash
892
- rivus --status-url http://127.0.0.1:17890/status --wait-receive accepted --wait-receive-message-id om_xxx --wait-receive-observed-after 2026-07-09T00:00:00.000Z --wait-timeout-ms 30000
893
- rivus --status-url http://127.0.0.1:17890/status --wait-receive handled --wait-receive-message-id om_xxx --wait-receive-text rivus-live-marker-123 --wait-receive-observed-after 2026-07-09T00:00:00.000Z --wait-timeout-ms 30000
894
- ```
895
-
896
- For the full local sequence from daemon startup through user-message send and exact callback verification, use the Feishu live inbound E2E runbook in [English](docs/en/operations/feishu-live-e2e.md) or [简体中文](docs/zh-CN/operations/feishu-live-e2e.md).
897
-
898
- If the wait times out, the CLI writes the last status JSON to stderr after `Last status:` so a live smoke check can still show whether the transport and worker loop were running, whether the queue had pending work, and whether `receive.lastAccepted` or `receive.lastHandled` was missing.
899
-
900
- ## Feishu Flow
901
-
902
- The core Feishu daemon remains transport-light. A WebSocket handler should acknowledge quickly, then enqueue work that calls the daemon use case:
903
-
904
- ```ts
905
- import { createFeishuAgentDaemon } from "@rivus/agent";
906
-
907
- const daemon = createFeishuAgentDaemon({
908
- agentId: "main",
909
- dedupe: true,
910
- harness,
911
- prepareRun: ({ runId }) => bindRunToCardTarget(runId),
912
- publish: (action) => sendActionToFeishuCard(action)
913
- });
914
- ```
915
-
916
- The daemon converts Feishu text and rich-text Post messages into prompt commands, preferring Post `content_v2` and flattening its textual paragraphs. It consumes harness `AgentRunUpdate` values and emits `update_text`, `finish`, `fail`, or `cancel` actions from projected run state. It accepts `/new` and `/reset` to stop any active run in the same conversation and advance a persisted session generation, so the next prompt starts with a fresh transcript; `/new /skill:<name> ...` combines the reset and the first prompt. A bare reset replies immediately and does not invoke the model. `/cancel` immediately cancels the active run in the current Feishu session, while `/cancel <runId>` keeps the stale-safe exact form used by running cards and callbacks. Both forms bypass the durable prompt queue, remain session-bound, and reply with an explicit stopped/no-active acknowledgement. `card.action.trigger` can cancel through the same exact path or resolve a persisted Human Interaction using only the trusted Feishu operator identity. Card-action tokens are process-locally deduplicated. Interaction decisions are persisted before the callback returns a toast and updated raw card; resolved cards contain no stale buttons. `prepareRun` is called on `agent_run_accepted`, before any stream action is published, so adapters can create a card and bind `runId` to `{ cardId, elementId }`. Publish effects are awaited in update order, and terminal publication replaces the entire card with a completed, failed, or cancelled projection. When daemon-level `dedupe` is enabled, a failed run releases its message-id marker so a retried delivery can run again. The Feishu daemon consumes an expected `AgentRunCancelled` outcome into a successful cancellation result, allowing the durable Inbox delivery to complete instead of becoming a dead letter; unexpected execution failures keep the normal retry and dead-letter policy.
917
-
918
- For Feishu's 3-second event handling requirement, put a queue in front of the daemon:
919
-
920
- ```ts
921
- import { createFeishuMessageQueue } from "@rivus/agent";
922
-
923
- const queue = createFeishuMessageQueue({
924
- handleMessage: (payload) => daemon.handleMessage(payload)
925
- });
926
-
927
- await Effect.runPromise(queue.accept(payload)); // return/ack quickly
928
- await Effect.runPromise(queue.drainOne()); // run agent work outside the ack path
929
- ```
930
-
931
- For the Feishu Node SDK, register the handler map returned by `createFeishuEventHandlers({ queue, cardActions })` and keep worker draining outside the SDK callback. Both the modern long-connection callback subscription and an HTTP webhook can deliver `card.action.trigger`; the configured WebSocket bootstrap therefore renders a `Stop generating` button and registers its exact-cancellation handler in the same `EventDispatcher` as `im.message.receive_v1`. The callback returns Feishu response JSON rather than the daemon's internal result; successful cancellation returns a toast immediately, then the ordered terminal action replaces the running card and removes its controls. Human Interaction responses include both `toast` and `{ card: { type: "raw", data } }` so the clicked card reaches its terminal projection immediately. `/cancel` and `/cancel <runId>` remain text fallbacks, not the primary card UX.
932
- If queued message handling fails, `drainOne()` leaves the message pending and fails the effect so the worker loop can report the error and retry on a later pass instead of silently dropping accepted work.
933
-
934
- For the common local-daemon path, use the runtime factory to keep that wiring in one place:
935
-
936
- ```ts
937
- import { Effect, Stream } from "effect";
938
- import { createFeishuAgentRuntime, createFeishuPeriodicFlush, createFeishuWorkerLoop } from "@rivus/agent";
939
-
940
- const periodicFlush = createFeishuPeriodicFlush({
941
- flush: () => publisher.flush(),
942
- intervalMs: 1000,
943
- sleep
944
- });
945
-
946
- const runtime = createFeishuAgentRuntime({
947
- agentId: "main",
948
- clock,
949
- loop: piLoop,
950
- periodicFlush,
951
- publish: (action) => sendActionToFeishuCard(action),
952
- runIds
953
- });
954
-
955
- await runtime.handlers["im.message.receive_v1"](payload); // ack path
956
- await Effect.runPromise(runtime.drainAvailable()); // worker path
957
- ```
958
-
959
- For local smoke tests and payload replays outside the SDK callback, `replayReceiveMessage(payload)` runs that same acceptance path and then drains available work in one Effect:
960
-
961
- ```ts
962
- const replay = await Effect.runPromise(runtime.replayReceiveMessage(payload));
963
- console.log(replay.intake.sessionKey, replay.accepted, replay.drained.drained);
964
- ```
965
-
966
- Use `runtime.replayReceiveMessage(payload, { sideEffects: "disabled" })` when the replay should still exercise intake, queueing, worker draining, harness execution, and session read models, but must not call the configured `prepareRun` or stream publisher.
967
-
968
- For a long-running daemon process, run the worker path in a background loop instead of calling it manually after each event:
969
-
970
- ```ts
971
- const workerLoop = createFeishuWorkerLoop({
972
- drainAvailable: () => runtime.drainAvailable(),
973
- intervalMs: 250,
974
- onError: (error) => console.error(error),
975
- sleep
976
- });
977
-
978
- workerLoop.start();
979
- ```
980
-
981
- Wrap the WebSocket transport and worker loop in a managed process so startup, rollback, and shutdown order stay consistent:
982
-
983
- ```ts
984
- import { createRivusDaemonProcess } from "@rivus/agent";
985
-
986
- const daemon = createRivusDaemonProcess({
987
- transport: wsDaemon,
988
- workerLoop
989
- });
990
-
991
- await Effect.runPromise(daemon.start());
992
- await Effect.runPromise(daemon.stop());
993
- ```
994
-
995
- To attach that runtime to the Feishu/Lark Node SDK long-connection mode, pass SDK-shaped `WSClient` and `EventDispatcher` objects to the WebSocket daemon adapter:
996
-
997
- ```ts
998
- import * as Lark from "@larksuiteoapi/node-sdk";
999
- import { createFeishuWebSocketDaemon } from "@rivus/agent";
1000
-
1001
- const wsDaemon = createFeishuWebSocketDaemon({
1002
- client: new Lark.WSClient({
1003
- appId: config.feishu.appId,
1004
- appSecret: config.feishu.appSecret
1005
- }),
1006
- eventDispatcher: new Lark.EventDispatcher({}),
1007
- runtime
1008
- });
1009
-
1010
- await wsDaemon.start();
1011
- ```
1012
-
1013
- The adapter calls `eventDispatcher.register(runtime.handlers)` and then `client.start({ eventDispatcher })`. It does not drain the worker inside the event callback, so the 3-second Feishu acknowledgement path stays short. On shutdown it calls the first available client cleanup method among `stop()`, `close()`, and `disconnect()`, which matches the official Lark SDK `WSClient.close()` shape while still supporting small test doubles. The bootstrap template lazily constructs the official `WSClient` only when the long connection starts, keeping `--status`, `--prompt`, and replay one-shot commands free of SDK constructor-time handles. Use `createFeishuWorkerLoop` beside the WebSocket daemon to consume accepted messages in the background.
1014
-
1015
- The complete [Pi and Feishu bootstrap template](examples/pi-feishu.bootstrap.ts) uses `createPiSessionRegistry` with `createPiAgentLoop({ disposeSessionAfterRun: false })` so Pi sessions are reused by Rivus `SessionKey` and disposed only during daemon shutdown. `createPiAgentLoop()` calls an optional `PiAgentSession.abort()` when `AgentLoopInput.abortSignal` is aborted, which lets reused sessions stop the current Pi turn without being disposed. By default, `createPiAgentLoop()` disposes either the returned handle or the session's own `dispose()` method after a run. The convenience `createPiSdkAgentLoop()` owns the SDK session it creates for a single run and disposes it when the run finishes.
1016
-
1017
- To wire Feishu CardKit card creation and publishing from config, compose the injected JSON request port and card target registry directly, or use `restoreConfiguredRivusDaemonBootstrap` to assemble the common daemon path while hydrating the harness from the event log:
1018
-
1019
- ```ts
1020
- import { Effect } from "effect";
1021
- import {
1022
- createConfiguredFeishuCardKitPublisher,
1023
- createConfiguredFeishuCardKitTargetPreparation,
1024
- createFeishuAgentRuntime,
1025
- createJsonlAgentEventLog,
1026
- createJsonFileFeishuCardTargetRegistry,
1027
- restoreConfiguredRivusDaemonBootstrap
1028
- } from "@rivus/agent";
1029
-
1030
- const targets = createJsonFileFeishuCardTargetRegistry({
1031
- filePath: "./.rivus/feishu-card-targets.json"
1032
- });
1033
- const eventLog = createJsonlAgentEventLog({
1034
- filePath: "./.rivus/agent-events.jsonl"
1035
- });
1036
-
1037
- const prepareRun = createConfiguredFeishuCardKitTargetPreparation({
1038
- config,
1039
- registry: targets,
1040
- request
1041
- });
1042
-
1043
- const publisher = createConfiguredFeishuCardKitPublisher({
1044
- config,
1045
- request,
1046
- resolveTarget: (runId) => targets.resolveTarget(runId),
1047
- sleep
1048
- });
1049
-
1050
- const runtime = createFeishuAgentRuntime({
1051
- agentId: config.agentId,
1052
- clock,
1053
- eventSinks: [eventLog],
1054
- loop: piLoop,
1055
- prepareRun,
1056
- publish: (action) => publisher.publish(action),
1057
- runIds
1058
- });
1059
-
1060
- const bootstrap = await Effect.runPromise(
1061
- restoreConfiguredRivusDaemonBootstrap({
1062
- cardTargets: targets,
1063
- clock,
1064
- config,
1065
- eventDispatcher,
1066
- eventLog,
1067
- loop: piLoop,
1068
- request,
1069
- runIds,
1070
- sleep,
1071
- websocketClient
1072
- })
1073
- );
1074
- ```
1075
-
1076
- The configured preparation helper creates a blue streaming CardKit entity, replies to the inbound Feishu message, and binds the run to the card target before any stream action is published. Its `Stop generating` button carries `{ rivus_action: "cancel_run", run_id, session_key }`; the long-connection dispatcher receives that modern callback and cancels only the still-active matching Run in the same Session. Completed, failed, and cancelled cards remove active controls. Deployments that have not subscribed to `card.action.trigger` may explicitly disable the button and retain `/cancel` as a compatibility fallback, but must not render a non-functional control. The configured publisher coalesces consecutive `update_text` actions by run, flushes the latest text before a terminal action, and exposes `flush(runId?)` for explicit worker-driven flushing. `createConfiguredRivusDaemonBootstrap` flushes pending text at `config.feishu.streamMinIntervalMs` by default; pass `flushIntervalMs` only to override that cadence. It also adds the event log, history restore helper, status reporter, optional status transport, WebSocket transport, worker loop, and managed process around those same pieces; `restoreConfiguredRivusDaemonBootstrap` first replays that event log as restored harness events so read models and same-session loop input are hydrated after restart. Local tests can use `createInMemoryFeishuCardTargetRegistry`; daemon processes should prefer `createJsonFileFeishuCardTargetRegistry` so run-to-card bindings survive restarts and malformed registry files fail loudly instead of being treated as empty state.
1077
-
1078
- ## Design Notes
1079
-
1080
- Read the maintainer architecture overview in [English](docs/en/architecture/README.md) or [简体中文](docs/zh-CN/architecture/README.md) for the current system boundaries, runtime semantics, reliability model, and integrations.
3
+ Compatibility package for the Rivus Agent API and the `rivus` executable. It projects the canonical Platform, Runtime,
4
+ and Gateway implementations through the historical root and subpath entrypoints.