@rivus/agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +1051 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +22 -0
- package/dist/index.d.ts +3423 -0
- package/dist/index.js +7337 -0
- package/dist/rivus-daemon-cli.js +2599 -0
- package/dist/rivus-plugin-registry.js +316 -0
- package/dist/rivus-plugin-testkit.d.ts +291 -0
- package/dist/rivus-plugin-testkit.js +62 -0
- package/dist/testing/index.d.ts +55 -0
- package/dist/testing/index.js +94 -0
- package/examples/current-weather.mjs +143 -0
- package/examples/html-drive-tools.mjs +262 -0
- package/examples/langfuse-drive-e2e.mjs +175 -0
- package/examples/pi-feishu-deployment.bootstrap.ts +542 -0
- package/examples/pi-feishu.bootstrap.ts +225 -0
- package/examples/rivus-agents.plugin.mjs +238 -0
- package/examples/rivus-langfuse-demo.config.json +36 -0
- package/examples/rivus.config.json +83 -0
- package/package.json +112 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { Effect } from "effect";
|
|
5
|
+
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
6
|
+
import {
|
|
7
|
+
AuthStorage,
|
|
8
|
+
createAgentSession,
|
|
9
|
+
DefaultResourceLoader,
|
|
10
|
+
ModelRegistry,
|
|
11
|
+
SessionManager
|
|
12
|
+
} from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import {
|
|
14
|
+
createAgentsMdInstructionsProvider,
|
|
15
|
+
createAgentHarness,
|
|
16
|
+
createAgentHarnessPooledRuntime,
|
|
17
|
+
createRivusMemoryToolDescriptor,
|
|
18
|
+
createConfiguredFeishuMarkdownMessageSender,
|
|
19
|
+
createConfiguredFeishuCardKitPublisher,
|
|
20
|
+
createConfiguredFeishuCardKitTargetPreparation,
|
|
21
|
+
createConfiguredFeishuHumanInteractionPresenter,
|
|
22
|
+
createConfiguredFeishuOpenApiClient,
|
|
23
|
+
createConfiguredFeishuTextReplySender,
|
|
24
|
+
createFeishuCotPublisher,
|
|
25
|
+
createFeishuDeploymentEndpoint,
|
|
26
|
+
createFeishuPresentationPreparation,
|
|
27
|
+
createFeishuCardDeliveryReconciler,
|
|
28
|
+
createHumanInteractionEndpointRegistry,
|
|
29
|
+
createHumanInteractionService,
|
|
30
|
+
createHumanInteractionToolApprovalGateway,
|
|
31
|
+
createJsonFetchRequest,
|
|
32
|
+
createJsonFileFeishuCardTargetRegistry,
|
|
33
|
+
createJsonlAgentEventLog,
|
|
34
|
+
createJsonlHumanInteractionRepository,
|
|
35
|
+
createLangfuseAgentTelemetry,
|
|
36
|
+
createLazyFeishuWebSocketEventDispatcher,
|
|
37
|
+
createPiAgentLoop,
|
|
38
|
+
createPiSkillRuntime,
|
|
39
|
+
createPiToolNameResolver,
|
|
40
|
+
createPiToolProxyDefinitions,
|
|
41
|
+
createPiSessionRegistry,
|
|
42
|
+
createRoutedHumanInteractionToolApprovalService,
|
|
43
|
+
createSessionScheduler,
|
|
44
|
+
createScheduledAutomation,
|
|
45
|
+
createSystemClock,
|
|
46
|
+
createToolBroker,
|
|
47
|
+
createUuidRunIds,
|
|
48
|
+
createWorkspaceRootHandle,
|
|
49
|
+
openJsonlFeishuCardDeliveryLedger,
|
|
50
|
+
openJsonlFeishuInboxRepository,
|
|
51
|
+
openJsonlAgentMemoryService,
|
|
52
|
+
openJsonAutomationTickRepository,
|
|
53
|
+
openJsonlToolOperationLedger,
|
|
54
|
+
openJsonlRecoveryControl,
|
|
55
|
+
resolveFeishuEndpointCredentials,
|
|
56
|
+
resolveLangfuseTelemetryConfig,
|
|
57
|
+
type CreateRivusDeploymentEndpointInput,
|
|
58
|
+
type CreateRivusDeploymentAutomationInput,
|
|
59
|
+
type CreateRivusDeploymentRuntimeInput,
|
|
60
|
+
type ConfiguredFeishuOpenApiResponse,
|
|
61
|
+
type FeishuWebSocketClient,
|
|
62
|
+
type RivusDaemonConfig,
|
|
63
|
+
type RivusDeploymentBootstrapContext,
|
|
64
|
+
type RivusThinkingLevel
|
|
65
|
+
} from "@rivus/agent";
|
|
66
|
+
|
|
67
|
+
type PiSessionOptions = NonNullable<Parameters<typeof createAgentSession>[0]>;
|
|
68
|
+
|
|
69
|
+
const STATE_DIR = process.env.RIVUS_DEPLOYMENT_STATE_DIR?.trim() || ".rivus/deployment";
|
|
70
|
+
const PI_AGENT_DIR = join(STATE_DIR, "pi-agent");
|
|
71
|
+
const PI_AUTH_FILE = join(STATE_DIR, "pi-auth.json");
|
|
72
|
+
const PI_MODELS_FILE = join(STATE_DIR, "pi-models.json");
|
|
73
|
+
const THINKING_LEVELS = new Set<RivusThinkingLevel>(["off", "minimal", "low", "medium", "high", "xhigh"]);
|
|
74
|
+
|
|
75
|
+
export async function createRivusDeploymentAdapters(context: RivusDeploymentBootstrapContext) {
|
|
76
|
+
await mkdir(PI_AGENT_DIR, { recursive: true });
|
|
77
|
+
const piOptions = await createPiSessionOptions(context);
|
|
78
|
+
const telemetryConfig = resolveLangfuseTelemetryConfig(context.env);
|
|
79
|
+
const telemetry = telemetryConfig ? createLangfuseAgentTelemetry(telemetryConfig) : undefined;
|
|
80
|
+
const memory = await openJsonlAgentMemoryService({
|
|
81
|
+
filePath: join(STATE_DIR, "memory", "agent-memory.jsonl")
|
|
82
|
+
});
|
|
83
|
+
const memoryTenantId = context.env.RIVUS_MEMORY_TENANT_ID?.trim() || "local";
|
|
84
|
+
const request = createJsonFetchRequest();
|
|
85
|
+
const createOpenApiClient = (config: RivusDaemonConfig) =>
|
|
86
|
+
createConfiguredFeishuOpenApiClient({
|
|
87
|
+
config,
|
|
88
|
+
request: (input) => request(input).pipe(Effect.map((response) => response as ConfiguredFeishuOpenApiResponse))
|
|
89
|
+
});
|
|
90
|
+
const interactionRegistry = createHumanInteractionEndpointRegistry();
|
|
91
|
+
const workspaceInstructions = await createAgentsMdInstructionsProvider().resolve({
|
|
92
|
+
maxBytes: 64 * 1024,
|
|
93
|
+
workingDirectory: ".",
|
|
94
|
+
workspaceRoot: await createWorkspaceRootHandle(process.cwd())
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
dispose: () => telemetry?.shutdown(),
|
|
99
|
+
createRecoveryControl: () =>
|
|
100
|
+
openJsonlRecoveryControl({
|
|
101
|
+
endpointsDirectory: join(STATE_DIR, "endpoints"),
|
|
102
|
+
instancesDirectory: join(STATE_DIR, "instances")
|
|
103
|
+
}),
|
|
104
|
+
createAutomation: async (input: CreateRivusDeploymentAutomationInput) => {
|
|
105
|
+
const credentials = resolveFeishuEndpointCredentials(input.deliveryEndpoint.credentialRef, context.env);
|
|
106
|
+
const config: RivusDaemonConfig = {
|
|
107
|
+
agentId: input.definition.agentId,
|
|
108
|
+
feishu: {
|
|
109
|
+
...credentials,
|
|
110
|
+
baseUrl: input.deliveryEndpoint.baseUrl,
|
|
111
|
+
streamMinIntervalMs: input.deliveryEndpoint.streamMinIntervalMs
|
|
112
|
+
},
|
|
113
|
+
pi: {}
|
|
114
|
+
};
|
|
115
|
+
const openApiClient = createOpenApiClient(config);
|
|
116
|
+
const sender = createConfiguredFeishuMarkdownMessageSender({
|
|
117
|
+
client: openApiClient,
|
|
118
|
+
config
|
|
119
|
+
});
|
|
120
|
+
const repository = await openJsonAutomationTickRepository({
|
|
121
|
+
filePath: join(
|
|
122
|
+
STATE_DIR,
|
|
123
|
+
"automations",
|
|
124
|
+
createHash("sha256").update(input.automationId).digest("hex").slice(0, 16),
|
|
125
|
+
"ticks.json"
|
|
126
|
+
)
|
|
127
|
+
});
|
|
128
|
+
const target = resolveAutomationTarget(input.definition.delivery.targetRef, context.env);
|
|
129
|
+
const automation = createScheduledAutomation({
|
|
130
|
+
automationId: input.automationId,
|
|
131
|
+
binding: {
|
|
132
|
+
agentId: input.definition.agentId,
|
|
133
|
+
bindingId: input.automationId,
|
|
134
|
+
delivery: {
|
|
135
|
+
endpointId: input.definition.delivery.endpointId,
|
|
136
|
+
targetKey: createHash("sha256").update(target).digest("hex"),
|
|
137
|
+
targetType: input.definition.delivery.targetType
|
|
138
|
+
},
|
|
139
|
+
schedule: input.definition.schedule,
|
|
140
|
+
servicePrincipalId: `automation:${input.automationId}`,
|
|
141
|
+
skillGrantRevision: input.definition.runtimeDefinition.skillGrantSet.revision,
|
|
142
|
+
skillIds: input.definition.template.requestedSkillIds,
|
|
143
|
+
templateId: input.definition.templateId,
|
|
144
|
+
timeZone: input.definition.timeZone,
|
|
145
|
+
toolIds: input.definition.template.requestedToolIds
|
|
146
|
+
},
|
|
147
|
+
createInput: input.definition.template.createInput,
|
|
148
|
+
deliver: ({ body, idempotencyKey }) =>
|
|
149
|
+
Effect.runPromise(
|
|
150
|
+
sender.send({
|
|
151
|
+
idempotencyKey,
|
|
152
|
+
receiveId: target,
|
|
153
|
+
receiveIdType: input.definition.delivery.targetType,
|
|
154
|
+
markdown: body
|
|
155
|
+
})
|
|
156
|
+
),
|
|
157
|
+
onError: () => {
|
|
158
|
+
console.error(`Scheduled Automation ${input.automationId} failed; it will retry`);
|
|
159
|
+
},
|
|
160
|
+
repository,
|
|
161
|
+
run: async (runInput) => readAutomationRunResult(await input.run(runInput))
|
|
162
|
+
});
|
|
163
|
+
return automation;
|
|
164
|
+
},
|
|
165
|
+
createEndpoint: async (input: CreateRivusDeploymentEndpointInput) => {
|
|
166
|
+
const credentials = resolveFeishuEndpointCredentials(input.definition.credentialRef, context.env);
|
|
167
|
+
const botOpenId = await resolveFeishuBotOpenId(credentials, input.definition.baseUrl);
|
|
168
|
+
const endpointState = join(STATE_DIR, "endpoints", input.endpointId);
|
|
169
|
+
const cardTargets = createJsonFileFeishuCardTargetRegistry({
|
|
170
|
+
filePath: join(endpointState, "feishu-card-targets.json")
|
|
171
|
+
});
|
|
172
|
+
const cardLedger = await openJsonlFeishuCardDeliveryLedger({
|
|
173
|
+
filePath: join(endpointState, "feishu-card-delivery.jsonl")
|
|
174
|
+
});
|
|
175
|
+
const inbox = await openJsonlFeishuInboxRepository({ filePath: join(endpointState, "feishu-inbox.jsonl") });
|
|
176
|
+
const config: RivusDaemonConfig = {
|
|
177
|
+
agentId: input.agentId,
|
|
178
|
+
feishu: {
|
|
179
|
+
...credentials,
|
|
180
|
+
baseUrl: input.definition.baseUrl,
|
|
181
|
+
streamMinIntervalMs: input.definition.streamMinIntervalMs
|
|
182
|
+
},
|
|
183
|
+
pi: {}
|
|
184
|
+
};
|
|
185
|
+
const openApiClient = createOpenApiClient(config);
|
|
186
|
+
const cotPublisher = input.definition.experimental?.cotMessages
|
|
187
|
+
? createFeishuCotPublisher({
|
|
188
|
+
baseUrl: resolveExperimentalCotBaseUrl(context.env),
|
|
189
|
+
client: openApiClient,
|
|
190
|
+
minIntervalMs: input.definition.streamMinIntervalMs
|
|
191
|
+
})
|
|
192
|
+
: undefined;
|
|
193
|
+
const interactions = createHumanInteractionService({
|
|
194
|
+
clock: { now: () => new Date().toISOString() },
|
|
195
|
+
presenter: createConfiguredFeishuHumanInteractionPresenter({
|
|
196
|
+
client: openApiClient,
|
|
197
|
+
config
|
|
198
|
+
}),
|
|
199
|
+
repository: createJsonlHumanInteractionRepository({
|
|
200
|
+
filePath: join(endpointState, "human-interactions.jsonl")
|
|
201
|
+
})
|
|
202
|
+
});
|
|
203
|
+
const publisher = createConfiguredFeishuCardKitPublisher({
|
|
204
|
+
agentName: input.agentId,
|
|
205
|
+
client: openApiClient,
|
|
206
|
+
config,
|
|
207
|
+
ledger: cardLedger,
|
|
208
|
+
resolveTarget: (runId) => cardTargets.resolveTarget(runId),
|
|
209
|
+
sleep
|
|
210
|
+
});
|
|
211
|
+
const endpointEvents = createJsonlAgentEventLog({
|
|
212
|
+
filePath: join(STATE_DIR, "instances", input.instanceId, "agent-events.jsonl")
|
|
213
|
+
});
|
|
214
|
+
await Effect.runPromise(
|
|
215
|
+
createFeishuCardDeliveryReconciler({
|
|
216
|
+
events: await Effect.runPromise(endpointEvents.readAll()),
|
|
217
|
+
ledger: cardLedger,
|
|
218
|
+
publish: (action) => publisher.publish(action)
|
|
219
|
+
}).reconcile()
|
|
220
|
+
);
|
|
221
|
+
const replies = createConfiguredFeishuTextReplySender({
|
|
222
|
+
client: openApiClient,
|
|
223
|
+
config
|
|
224
|
+
});
|
|
225
|
+
const prepareCardTarget = createConfiguredFeishuCardKitTargetPreparation({
|
|
226
|
+
client: openApiClient,
|
|
227
|
+
config,
|
|
228
|
+
registry: cardTargets,
|
|
229
|
+
title: input.agentId
|
|
230
|
+
});
|
|
231
|
+
const endpoint = createFeishuDeploymentEndpoint({
|
|
232
|
+
agentId: input.agentId,
|
|
233
|
+
botOpenId,
|
|
234
|
+
cancel: input.cancel,
|
|
235
|
+
endpointId: input.endpointId,
|
|
236
|
+
eventDispatcher: createLazyFeishuWebSocketEventDispatcher(() => new Lark.EventDispatcher({})),
|
|
237
|
+
groupPolicy: input.definition.groupPolicy,
|
|
238
|
+
handle: input.handle,
|
|
239
|
+
inboxRepository: inbox,
|
|
240
|
+
interactions,
|
|
241
|
+
maxPendingMessages: 100,
|
|
242
|
+
memoryTenantId,
|
|
243
|
+
onCapacityExceeded: (payload) =>
|
|
244
|
+
replies.reply(payload.event.message.message_id, "Rivus is busy. Please retry in a moment."),
|
|
245
|
+
prepareRun: createFeishuPresentationPreparation({
|
|
246
|
+
...(cotPublisher ? { cotPublisher } : {}),
|
|
247
|
+
prepareCardTarget,
|
|
248
|
+
reportCotError: (operation, error) => reportCotError(input.endpointId, operation, error)
|
|
249
|
+
}),
|
|
250
|
+
publish: (action) => publisher.publish(action),
|
|
251
|
+
...(cotPublisher
|
|
252
|
+
? {
|
|
253
|
+
publishRunUpdate: (update) =>
|
|
254
|
+
cotPublisher
|
|
255
|
+
.publish(update)
|
|
256
|
+
.pipe(Effect.catchAll((error) => reportCotError(input.endpointId, "update", error)))
|
|
257
|
+
}
|
|
258
|
+
: {}),
|
|
259
|
+
sessionNamespace: input.definition.sessionNamespace,
|
|
260
|
+
sleep,
|
|
261
|
+
websocketClient: createLazyFeishuWebSocketClient(credentials, input.definition.baseUrl),
|
|
262
|
+
workerConcurrency: 4
|
|
263
|
+
});
|
|
264
|
+
interactionRegistry.register(input.endpointId, interactions);
|
|
265
|
+
return endpoint;
|
|
266
|
+
},
|
|
267
|
+
createRuntime: async (input: CreateRivusDeploymentRuntimeInput) => {
|
|
268
|
+
const instanceState = join(STATE_DIR, "instances", input.instanceId);
|
|
269
|
+
await mkdir(instanceState, { recursive: true });
|
|
270
|
+
const eventLog = createJsonlAgentEventLog({ filePath: join(instanceState, "agent-events.jsonl") });
|
|
271
|
+
const initialEvents = await Effect.runPromise(eventLog.readAll());
|
|
272
|
+
const broker = createToolBroker({
|
|
273
|
+
approvals: createRoutedHumanInteractionToolApprovalService(interactionRegistry),
|
|
274
|
+
catalog: input.catalog,
|
|
275
|
+
...(input.definition.memory.tool ? { hostTools: [createRivusMemoryToolDescriptor({ memory })] } : {}),
|
|
276
|
+
operations: await openJsonlToolOperationLedger({ filePath: join(instanceState, "tool-operations.jsonl") }),
|
|
277
|
+
policy: { current: async () => ({ epoch: 1, revokedToolIds: [] }) }
|
|
278
|
+
});
|
|
279
|
+
const resolveToolName = createPiToolNameResolver(input.definition.tools);
|
|
280
|
+
const skillRuntime = createPiSkillRuntime(input.definition.skills);
|
|
281
|
+
const sessionRegistry = createPiSessionRegistry({
|
|
282
|
+
createSession: async (firstInput) => {
|
|
283
|
+
let activeInput = firstInput;
|
|
284
|
+
const customTools = [
|
|
285
|
+
...createPiToolProxyDefinitions({
|
|
286
|
+
agentId: input.agentId,
|
|
287
|
+
approvals: createHumanInteractionToolApprovalGateway({ registry: interactionRegistry }),
|
|
288
|
+
broker,
|
|
289
|
+
getActiveInput: () => activeInput,
|
|
290
|
+
instanceId: input.instanceId,
|
|
291
|
+
memoryScopes: input.definition.memory.scopes,
|
|
292
|
+
toolGrantSet: input.definition.toolGrantSet,
|
|
293
|
+
tools: input.definition.tools
|
|
294
|
+
}),
|
|
295
|
+
...(skillRuntime.tool ? [skillRuntime.tool] : [])
|
|
296
|
+
];
|
|
297
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
298
|
+
agentDir: PI_AGENT_DIR,
|
|
299
|
+
appendSystemPromptOverride: () =>
|
|
300
|
+
[workspaceInstructions.content, skillRuntime.prompt].filter((content) => content.length > 0),
|
|
301
|
+
cwd: process.cwd(),
|
|
302
|
+
noContextFiles: true,
|
|
303
|
+
noExtensions: true,
|
|
304
|
+
noPromptTemplates: true,
|
|
305
|
+
noSkills: true,
|
|
306
|
+
noThemes: true,
|
|
307
|
+
systemPromptOverride: () => input.definition.systemPrompt
|
|
308
|
+
});
|
|
309
|
+
await resourceLoader.reload();
|
|
310
|
+
const result = await createAgentSession({
|
|
311
|
+
...piOptions,
|
|
312
|
+
customTools,
|
|
313
|
+
resourceLoader,
|
|
314
|
+
sessionManager: SessionManager.create(process.cwd(), join(instanceState, "sessions")),
|
|
315
|
+
tools: customTools.map(({ name }) => name)
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
activate: (loopInput) => {
|
|
319
|
+
activeInput = loopInput;
|
|
320
|
+
},
|
|
321
|
+
dispose: () => result.session.dispose(),
|
|
322
|
+
resolveToolName,
|
|
323
|
+
session: result.session
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
const loop = createPiAgentLoop({
|
|
328
|
+
disposeSessionAfterRun: false,
|
|
329
|
+
...(telemetry ? { modelContentObserver: telemetry.modelContentObserver } : {}),
|
|
330
|
+
resolveSession: (loopInput) => sessionRegistry.resolve(loopInput)
|
|
331
|
+
});
|
|
332
|
+
const scheduler = createSessionScheduler({
|
|
333
|
+
createRuntime: (sessionKey) =>
|
|
334
|
+
createAgentHarnessPooledRuntime(
|
|
335
|
+
createAgentHarness({
|
|
336
|
+
clock: createSystemClock(),
|
|
337
|
+
eventSinks: telemetry ? [eventLog, telemetry.sink] : [eventLog],
|
|
338
|
+
initialEvents: eventsForSession(initialEvents, sessionKey),
|
|
339
|
+
loop,
|
|
340
|
+
runIds: createUuidRunIds()
|
|
341
|
+
})
|
|
342
|
+
),
|
|
343
|
+
maxConcurrentSessions: 4,
|
|
344
|
+
maxQueuedRuns: 32
|
|
345
|
+
});
|
|
346
|
+
return {
|
|
347
|
+
...scheduler,
|
|
348
|
+
dispose: async () => {
|
|
349
|
+
await scheduler.dispose?.();
|
|
350
|
+
await sessionRegistry.disposeAll();
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function eventsForSession<T extends { readonly runId: string; readonly sessionKey?: string }>(
|
|
358
|
+
events: ReadonlyArray<T>,
|
|
359
|
+
sessionKey: string
|
|
360
|
+
): ReadonlyArray<T> {
|
|
361
|
+
const runIds = new Set(events.filter((event) => event.sessionKey === sessionKey).map((event) => event.runId));
|
|
362
|
+
return events.filter((event) => runIds.has(event.runId));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function resolveAutomationTarget(targetRef: string, env: Readonly<Record<string, string | undefined>>): string {
|
|
366
|
+
if (!targetRef.startsWith("env:")) throw new Error("Automation delivery targetRef must use env:<VARIABLE>");
|
|
367
|
+
const variable = targetRef.slice("env:".length);
|
|
368
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(variable)) throw new Error(`Invalid Automation delivery targetRef: ${targetRef}`);
|
|
369
|
+
const target = env[variable]?.trim();
|
|
370
|
+
if (!target) throw new Error(`${variable} is required`);
|
|
371
|
+
return target;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function resolveExperimentalCotBaseUrl(env: Readonly<Record<string, string | undefined>>): string {
|
|
375
|
+
const baseUrl = env.RIVUS_FEISHU_COT_BASE_URL?.trim();
|
|
376
|
+
if (!baseUrl) {
|
|
377
|
+
throw new Error("RIVUS_FEISHU_COT_BASE_URL is required when experimental.cotMessages is enabled");
|
|
378
|
+
}
|
|
379
|
+
return baseUrl;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function reportCotError(endpointId: string, operation: string, error: unknown): Effect.Effect<void> {
|
|
383
|
+
return Effect.sync(() => {
|
|
384
|
+
const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
385
|
+
console.error(`Experimental Feishu COT ${operation} failed for endpoint ${endpointId}: ${detail}`);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function readAutomationRunResult(result: unknown): { readonly body: string; readonly runId: string } {
|
|
390
|
+
if (
|
|
391
|
+
result !== null &&
|
|
392
|
+
typeof result === "object" &&
|
|
393
|
+
"finalText" in result &&
|
|
394
|
+
typeof result.finalText === "string" &&
|
|
395
|
+
result.finalText.trim() !== "" &&
|
|
396
|
+
"runId" in result &&
|
|
397
|
+
typeof result.runId === "string" &&
|
|
398
|
+
result.runId.trim() !== ""
|
|
399
|
+
) {
|
|
400
|
+
return { body: result.finalText, runId: result.runId };
|
|
401
|
+
}
|
|
402
|
+
throw new Error("Scheduled Automation Agent Run did not produce a runId and final text");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function createLazyFeishuWebSocketClient(
|
|
406
|
+
credentials: {
|
|
407
|
+
readonly appId: string;
|
|
408
|
+
readonly appSecret: string;
|
|
409
|
+
},
|
|
410
|
+
domain: string
|
|
411
|
+
): FeishuWebSocketClient {
|
|
412
|
+
let client: Lark.WSClient | undefined;
|
|
413
|
+
let connected = false;
|
|
414
|
+
return {
|
|
415
|
+
connected: () => connected,
|
|
416
|
+
close: () => {
|
|
417
|
+
client?.close();
|
|
418
|
+
client = undefined;
|
|
419
|
+
connected = false;
|
|
420
|
+
},
|
|
421
|
+
start: (options) =>
|
|
422
|
+
new Promise<void>((resolve, reject) => {
|
|
423
|
+
let settled = false;
|
|
424
|
+
const settle = (callback: () => void) => {
|
|
425
|
+
if (settled) return;
|
|
426
|
+
settled = true;
|
|
427
|
+
clearTimeout(timeout);
|
|
428
|
+
callback();
|
|
429
|
+
};
|
|
430
|
+
const timeout = setTimeout(
|
|
431
|
+
() => settle(() => reject(new Error("Feishu WebSocket handshake timed out after 15000ms"))),
|
|
432
|
+
15_000
|
|
433
|
+
);
|
|
434
|
+
client ??= new Lark.WSClient({
|
|
435
|
+
...credentials,
|
|
436
|
+
domain,
|
|
437
|
+
handshakeTimeoutMs: 10_000,
|
|
438
|
+
onError: (error) => {
|
|
439
|
+
connected = false;
|
|
440
|
+
settle(() => reject(error));
|
|
441
|
+
},
|
|
442
|
+
onReady: () => {
|
|
443
|
+
connected = true;
|
|
444
|
+
settle(resolve);
|
|
445
|
+
},
|
|
446
|
+
onReconnected: () => {
|
|
447
|
+
connected = true;
|
|
448
|
+
},
|
|
449
|
+
onReconnecting: () => {
|
|
450
|
+
connected = false;
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
void client.start(options as Parameters<Lark.WSClient["start"]>[0]).catch((error: unknown) => {
|
|
454
|
+
connected = false;
|
|
455
|
+
settle(() => reject(error));
|
|
456
|
+
});
|
|
457
|
+
})
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function resolveFeishuBotOpenId(
|
|
462
|
+
credentials: {
|
|
463
|
+
readonly appId: string;
|
|
464
|
+
readonly appSecret: string;
|
|
465
|
+
},
|
|
466
|
+
domain: string
|
|
467
|
+
): Promise<string> {
|
|
468
|
+
const response = (await new Lark.Client({ ...credentials, domain }).request({
|
|
469
|
+
method: "GET",
|
|
470
|
+
url: "/open-apis/bot/v3/info"
|
|
471
|
+
})) as { readonly bot?: { readonly open_id?: string }; readonly code?: number; readonly msg?: string };
|
|
472
|
+
const openId = response.bot?.open_id;
|
|
473
|
+
if (response.code !== 0 || !openId) {
|
|
474
|
+
throw new Error(`Feishu bot identity lookup failed: ${response.msg ?? "response missing bot.open_id"}`);
|
|
475
|
+
}
|
|
476
|
+
return openId;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function createPiSessionOptions(context: RivusDeploymentBootstrapContext): Promise<PiSessionOptions> {
|
|
480
|
+
const authStorage = AuthStorage.create(PI_AUTH_FILE);
|
|
481
|
+
const modelReference = optional(context.env.PI_MODEL);
|
|
482
|
+
const configuredModel = modelReference ? parseModelReference(modelReference) : undefined;
|
|
483
|
+
const provider = configuredModel?.provider;
|
|
484
|
+
const apiKey = await resolvePiApiKey(context.env);
|
|
485
|
+
const baseUrl = optional(context.env.PI_BASE_URL);
|
|
486
|
+
if ((apiKey || baseUrl) && !provider) {
|
|
487
|
+
throw new Error("PI_API_KEY and PI_BASE_URL require PI_MODEL in provider/model form");
|
|
488
|
+
}
|
|
489
|
+
if (apiKey) authStorage.setRuntimeApiKey(provider!, apiKey);
|
|
490
|
+
if (baseUrl) await writeProviderBaseUrlOverride(provider!, baseUrl);
|
|
491
|
+
const modelRegistry = ModelRegistry.create(authStorage, baseUrl ? PI_MODELS_FILE : undefined);
|
|
492
|
+
const model = configuredModel ? modelRegistry.find(configuredModel.provider, configuredModel.modelId) : undefined;
|
|
493
|
+
if (modelReference && !model) throw new Error(`PI_MODEL ${modelReference} was not found in the Pi model registry`);
|
|
494
|
+
const thinkingLevel = readThinkingLevel(context.env.PI_THINKING_LEVEL);
|
|
495
|
+
return {
|
|
496
|
+
authStorage,
|
|
497
|
+
cwd: process.cwd(),
|
|
498
|
+
modelRegistry,
|
|
499
|
+
...(model ? { model } : {}),
|
|
500
|
+
...(thinkingLevel ? { thinkingLevel } : {})
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function resolvePiApiKey(env: Readonly<Record<string, string | undefined>>): Promise<string | undefined> {
|
|
505
|
+
const inline = optional(env.PI_API_KEY);
|
|
506
|
+
const filePath = optional(env.PI_API_KEY_FILE);
|
|
507
|
+
if (inline && filePath) throw new Error("PI_API_KEY and PI_API_KEY_FILE cannot both be set");
|
|
508
|
+
if (inline) return inline;
|
|
509
|
+
if (!filePath) return undefined;
|
|
510
|
+
const contents = optional(await readFile(filePath, "utf8"));
|
|
511
|
+
if (!contents) throw new Error("PI_API_KEY_FILE must not be empty");
|
|
512
|
+
return contents;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function optional(value: string | undefined): string | undefined {
|
|
516
|
+
return value?.trim() || undefined;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function parseModelReference(reference: string): { readonly modelId: string; readonly provider: string } {
|
|
520
|
+
const separator = reference.indexOf("/");
|
|
521
|
+
if (separator <= 0 || separator === reference.length - 1) {
|
|
522
|
+
throw new Error("PI_MODEL must use provider/model form, for example zai/glm-5.1");
|
|
523
|
+
}
|
|
524
|
+
return { modelId: reference.slice(separator + 1), provider: reference.slice(0, separator) };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function readThinkingLevel(value: string | undefined): RivusThinkingLevel | undefined {
|
|
528
|
+
const level = optional(value);
|
|
529
|
+
if (!level) return undefined;
|
|
530
|
+
if (!THINKING_LEVELS.has(level as RivusThinkingLevel)) {
|
|
531
|
+
throw new Error("PI_THINKING_LEVEL must be one of off, minimal, low, medium, high, xhigh");
|
|
532
|
+
}
|
|
533
|
+
return level as RivusThinkingLevel;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function sleep(ms: number): Effect.Effect<void> {
|
|
537
|
+
return Effect.promise(() => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function writeProviderBaseUrlOverride(provider: string, baseUrl: string): Promise<void> {
|
|
541
|
+
await writeFile(PI_MODELS_FILE, `${JSON.stringify({ providers: { [provider]: { baseUrl } } }, null, 2)}\n`, "utf8");
|
|
542
|
+
}
|