426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
334 lines
10 KiB
TypeScript
334 lines
10 KiB
TypeScript
/**
|
|
* Handles the get_activity Linear op. Extracts time-range, action, resource, and
|
|
* success filters from the message via the getActivity prompt, reads the matching
|
|
* slice of LinearService's in-memory activity log, and formats it into the reply.
|
|
*/
|
|
import {
|
|
type Action,
|
|
type ActionResult,
|
|
type HandlerCallback,
|
|
type HandlerOptions,
|
|
type IAgentRuntime,
|
|
logger,
|
|
type Memory,
|
|
ModelType,
|
|
type State,
|
|
} from "@elizaos/core";
|
|
import { getActivityTemplate } from "../prompts.js";
|
|
import type { LinearService } from "../services/linear";
|
|
import { getLinearAccountId, linearAccountIdParameter } from "./account-options";
|
|
import { formatUnknownError, getMessageSource } from "./message-source";
|
|
import {
|
|
getNumberValue,
|
|
getRecordValue,
|
|
getStringArrayValue,
|
|
getStringValue,
|
|
parseLinearPromptResponse,
|
|
} from "./parseLinearPrompt.js";
|
|
import { validateLinearActionIntent } from "./validate-linear-intent";
|
|
|
|
function formatActivityDetail(value: unknown): string {
|
|
if (value === null || value === undefined) {
|
|
return "none";
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return value.map(formatActivityDetail).join(", ");
|
|
}
|
|
if (typeof value === "object") {
|
|
return Object.entries(value as Record<string, unknown>)
|
|
.map(([key, nestedValue]) => `${key}=${formatActivityDetail(nestedValue)}`)
|
|
.join("; ");
|
|
}
|
|
return String(value);
|
|
}
|
|
|
|
type ActivityParams = {
|
|
filters?: Record<string, unknown>;
|
|
limit?: number;
|
|
};
|
|
|
|
export const getActivityAction: Action = {
|
|
name: "GET_LINEAR_ACTIVITY",
|
|
contexts: ["tasks", "connectors", "automation"],
|
|
contextGate: { anyOf: ["tasks", "connectors", "automation"] },
|
|
roleGate: { minRole: "USER" },
|
|
description: "Get recent Linear activity log with filters.",
|
|
descriptionCompressed: "get recent Linear activity log w/ optional filter",
|
|
similes: [
|
|
"get-linear-activity",
|
|
"show-linear-activity",
|
|
"view-linear-activity",
|
|
"check-linear-activity",
|
|
],
|
|
parameters: [
|
|
{
|
|
name: "filters",
|
|
description:
|
|
"Activity filters: fromDate ISO timestamp, action, resource_type, resource_id, success.",
|
|
required: false,
|
|
schema: { type: "object" as const },
|
|
},
|
|
{
|
|
name: "limit",
|
|
description: "Max activity entries.",
|
|
required: false,
|
|
schema: { type: "number" as const },
|
|
},
|
|
linearAccountIdParameter,
|
|
],
|
|
|
|
examples: [
|
|
[
|
|
{
|
|
name: "User",
|
|
content: {
|
|
text: "Show me recent Linear activity",
|
|
},
|
|
},
|
|
{
|
|
name: "Assistant",
|
|
content: {
|
|
text: "I'll show you the recent Linear activity.",
|
|
actions: ["GET_LINEAR_ACTIVITY"],
|
|
},
|
|
},
|
|
],
|
|
[
|
|
{
|
|
name: "User",
|
|
content: {
|
|
text: "What happened in Linear today?",
|
|
},
|
|
},
|
|
{
|
|
name: "Assistant",
|
|
content: {
|
|
text: "Let me check today's Linear activity for you.",
|
|
actions: ["GET_LINEAR_ACTIVITY"],
|
|
},
|
|
},
|
|
],
|
|
[
|
|
{
|
|
name: "User",
|
|
content: {
|
|
text: "Show me what issues John created this week",
|
|
},
|
|
},
|
|
{
|
|
name: "Assistant",
|
|
content: {
|
|
text: "I'll find the issues John created this week.",
|
|
actions: ["GET_LINEAR_ACTIVITY"],
|
|
},
|
|
},
|
|
],
|
|
],
|
|
|
|
validate: async (runtime: IAgentRuntime, message: Memory, state?: State): Promise<boolean> =>
|
|
validateLinearActionIntent(runtime, message, state),
|
|
|
|
async handler(
|
|
runtime: IAgentRuntime,
|
|
message: Memory,
|
|
_state?: State,
|
|
_options?: HandlerOptions,
|
|
callback?: HandlerCallback
|
|
): Promise<ActionResult> {
|
|
try {
|
|
const linearService = runtime.getService<LinearService>("linear");
|
|
if (!linearService) {
|
|
throw new Error("Linear service not available");
|
|
}
|
|
const accountId = getLinearAccountId(runtime, _options);
|
|
|
|
const content = message.content.text || "";
|
|
const params = (_options?.parameters ?? {}) as ActivityParams;
|
|
const filters: Record<string, unknown> = { ...(params.filters ?? {}) };
|
|
let limit =
|
|
typeof params.limit === "number" && Number.isFinite(params.limit)
|
|
? Math.max(1, Math.floor(params.limit))
|
|
: 10;
|
|
|
|
if (content) {
|
|
const prompt = getActivityTemplate.replace("{{userMessage}}", content);
|
|
const response = await runtime.useModel(ModelType.TEXT_LARGE, {
|
|
prompt: prompt,
|
|
});
|
|
|
|
if (response) {
|
|
try {
|
|
const parsed = parseLinearPromptResponse(response);
|
|
if (Object.keys(parsed).length === 0) {
|
|
throw new Error("No fields found in model response");
|
|
}
|
|
|
|
const timeRange = getRecordValue(parsed.timeRange);
|
|
if (timeRange) {
|
|
const now = new Date();
|
|
let fromDate: Date | undefined;
|
|
|
|
const from = getStringValue(timeRange.from);
|
|
const period = getStringValue(timeRange.period);
|
|
if (from) {
|
|
fromDate = new Date(from);
|
|
} else if (period) {
|
|
switch (period) {
|
|
case "today":
|
|
fromDate = new Date(now.setHours(0, 0, 0, 0));
|
|
break;
|
|
case "yesterday":
|
|
fromDate = new Date(now.setDate(now.getDate() - 1));
|
|
fromDate.setHours(0, 0, 0, 0);
|
|
break;
|
|
case "this-week":
|
|
fromDate = new Date(now.setDate(now.getDate() - now.getDay()));
|
|
fromDate.setHours(0, 0, 0, 0);
|
|
break;
|
|
case "last-week":
|
|
fromDate = new Date(now.setDate(now.getDate() - now.getDay() - 7));
|
|
fromDate.setHours(0, 0, 0, 0);
|
|
break;
|
|
case "this-month":
|
|
fromDate = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (fromDate) {
|
|
filters.fromDate = fromDate.toISOString();
|
|
}
|
|
}
|
|
|
|
const actionTypes = getStringArrayValue(parsed.actionTypes);
|
|
if (actionTypes && actionTypes.length > 0) {
|
|
filters.action = actionTypes[0];
|
|
}
|
|
|
|
const resourceTypes = getStringArrayValue(parsed.resourceTypes);
|
|
if (resourceTypes && resourceTypes.length > 0) {
|
|
filters.resource_type = resourceTypes[0];
|
|
}
|
|
|
|
const resourceId = getStringValue(parsed.resourceId);
|
|
if (resourceId) {
|
|
filters.resource_id = resourceId;
|
|
}
|
|
|
|
const successFilter = getStringValue(parsed.successFilter);
|
|
if (successFilter && successFilter !== "all") {
|
|
filters.success = successFilter === "success";
|
|
}
|
|
|
|
limit = getNumberValue(parsed.limit) || 10;
|
|
} catch (parseError) {
|
|
logger.warn("Failed to parse activity filters:", formatUnknownError(parseError));
|
|
}
|
|
}
|
|
}
|
|
|
|
let activity = linearService.getActivityLog(limit * 2, filters, accountId);
|
|
|
|
if (filters.fromDate) {
|
|
const fromDateValue = filters.fromDate;
|
|
const fromDate =
|
|
typeof fromDateValue === "string"
|
|
? fromDateValue
|
|
: fromDateValue instanceof Date
|
|
? fromDateValue.toISOString()
|
|
: String(fromDateValue);
|
|
const fromTime = new Date(fromDate).getTime();
|
|
if (!Number.isNaN(fromTime)) {
|
|
activity = activity.filter((item) => new Date(item.timestamp).getTime() >= fromTime);
|
|
}
|
|
}
|
|
|
|
activity = activity.slice(0, limit);
|
|
|
|
if (activity.length === 0) {
|
|
const noActivityMessage = filters.fromDate
|
|
? `No Linear activity found for the specified filters.`
|
|
: "No recent Linear activity found.";
|
|
await callback?.({
|
|
text: noActivityMessage,
|
|
source: getMessageSource(message),
|
|
});
|
|
return {
|
|
text: noActivityMessage,
|
|
success: true,
|
|
data: {
|
|
activity: [],
|
|
accountId,
|
|
},
|
|
};
|
|
}
|
|
|
|
const activityText = activity
|
|
.map((item, index) => {
|
|
const time = new Date(item.timestamp).toLocaleString();
|
|
const status = item.success ? "✅" : "❌";
|
|
const details = Object.entries(item.details)
|
|
.filter(([key]) => key !== "filters")
|
|
.map(([key, value]) => `${key}: ${formatActivityDetail(value)}`)
|
|
.join(", ");
|
|
|
|
return `${index + 1}. ${status} ${item.action} on ${item.resource_type} ${item.resource_id}\n Time: ${time}\n ${details ? `Details: ${details}` : ""}${item.error ? `\n Error: ${item.error}` : ""}`;
|
|
})
|
|
.join("\n\n");
|
|
|
|
const headerText = filters.fromDate
|
|
? `📊 Linear activity ${content}:`
|
|
: "📊 Recent Linear activity:";
|
|
|
|
const resultMessage = `${headerText}\n\n${activityText}`;
|
|
await callback?.({
|
|
text: resultMessage,
|
|
source: getMessageSource(message),
|
|
});
|
|
|
|
return {
|
|
text: `Found ${activity.length} activity item${activity.length === 1 ? "" : "s"}`,
|
|
success: true,
|
|
data: {
|
|
activity: activity.map((item) => ({
|
|
id: item.id,
|
|
action: item.action,
|
|
resource_type: item.resource_type,
|
|
resource_id: item.resource_id,
|
|
success: item.success,
|
|
error: item.error,
|
|
details: formatActivityDetail(item.details),
|
|
timestamp:
|
|
typeof item.timestamp === "string"
|
|
? item.timestamp
|
|
: new Date(item.timestamp).toISOString(),
|
|
})) as Array<Record<string, string | boolean | undefined>>,
|
|
filters: filters
|
|
? {
|
|
...filters,
|
|
fromDate: filters.fromDate
|
|
? typeof filters.fromDate === "string"
|
|
? filters.fromDate
|
|
: String(filters.fromDate)
|
|
: undefined,
|
|
}
|
|
: undefined,
|
|
count: activity.length,
|
|
accountId,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
logger.error("Failed to get activity:", formatUnknownError(error));
|
|
const errorMessage = `❌ Failed to get activity: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
await callback?.({
|
|
text: errorMessage,
|
|
source: getMessageSource(message),
|
|
});
|
|
return {
|
|
text: errorMessage,
|
|
success: false,
|
|
};
|
|
}
|
|
},
|
|
};
|