From 02fef45f56d5515d7bc873dca790ecce3408d9f0 Mon Sep 17 00:00:00 2001 From: Ricky Schema Cascade Date: Tue, 2 Jun 2026 21:59:07 +0200 Subject: [PATCH 1/2] feat(runtime): type Linear agent session events --- packages/persona-kit/package.json | 1 + packages/persona-kit/src/define.test.ts | 7 +- packages/persona-kit/src/define.ts | 3 +- packages/persona-kit/src/triggers.test.ts | 7 +- packages/persona-kit/src/triggers.ts | 9 ++ packages/runtime/src/define-agent.test.ts | 83 ++++++++++++++++- packages/runtime/src/define-agent.ts | 21 ++++- packages/runtime/src/index.ts | 17 ++++ packages/runtime/src/types.ts | 107 +++++++++++++++++++++- pnpm-lock.yaml | 29 ++++++ 10 files changed, 278 insertions(+), 6 deletions(-) diff --git a/packages/persona-kit/package.json b/packages/persona-kit/package.json index bd1e2b88..727de173 100644 --- a/packages/persona-kit/package.json +++ b/packages/persona-kit/package.json @@ -38,6 +38,7 @@ }, "dependencies": { "@relayfile/adapter-core": "^0.3.26", + "@relayfile/adapter-linear": "^0.3.8", "@relayfile/local-mount": "^0.7.24" }, "devDependencies": { diff --git a/packages/persona-kit/src/define.test.ts b/packages/persona-kit/src/define.test.ts index 3e3a7fb8..4addb54f 100644 --- a/packages/persona-kit/src/define.test.ts +++ b/packages/persona-kit/src/define.test.ts @@ -61,7 +61,12 @@ test('TypedTriggerMap gives per-provider event autocomplete; arbitrary providers { on: 'pull_request.opened' }, { on: 'off_registry.github_event' } ], - linear: [{ on: 'issue.create' }], + linear: [ + { on: 'issue.create' }, + { on: 'AgentSessionEvent.created' }, + { on: 'AgentSessionEvent.prompted' }, + { on: 'AppUserNotification.issueCommentMention' } + ], slack: [{ on: 'message.created' }], customProvider: [{ on: 'custom.event' }] }; diff --git a/packages/persona-kit/src/define.ts b/packages/persona-kit/src/define.ts index 038c276d..b4ec0ce4 100644 --- a/packages/persona-kit/src/define.ts +++ b/packages/persona-kit/src/define.ts @@ -14,9 +14,10 @@ import type { import type { KnownProviderName, KnownTriggerName } from './triggers.js'; import type { ScopeKey, ScopeKeyProvider } from './scope-keys.js'; import type { KnownPersonaTag } from './constants.js'; +import type { LinearAgentWebhookEvent } from '@relayfile/adapter-linear/types'; export type TriggerNameFor

= P extends KnownProviderName - ? KnownTriggerName

| (string & {}) + ? (P extends 'linear' ? KnownTriggerName

| LinearAgentWebhookEvent : KnownTriggerName

) | (string & {}) : string; export interface TypedTrigger

{ diff --git a/packages/persona-kit/src/triggers.test.ts b/packages/persona-kit/src/triggers.test.ts index a7043b82..390506cb 100644 --- a/packages/persona-kit/src/triggers.test.ts +++ b/packages/persona-kit/src/triggers.test.ts @@ -31,7 +31,12 @@ test('lintTriggers returns no issues for known providers and known triggers', () { on: 'pull_request.opened' }, { on: 'pull_request_review_comment.created' } ], - linear: [{ on: 'issue.create' }], + linear: [ + { on: 'issue.create' }, + { on: 'AgentSessionEvent.created' }, + { on: 'AgentSessionEvent.prompted' }, + { on: 'AppUserNotification.issueCommentMention' } + ], slack: [{ on: 'message.created' }] }) ); diff --git a/packages/persona-kit/src/triggers.ts b/packages/persona-kit/src/triggers.ts index 0c050af5..3c2b2e3e 100644 --- a/packages/persona-kit/src/triggers.ts +++ b/packages/persona-kit/src/triggers.ts @@ -1,5 +1,6 @@ import type { AgentSpec } from './types.js'; import { KNOWN_TRIGGER_CATALOG } from '@relayfile/adapter-core/triggers'; +import { LINEAR_AGENT_WEBHOOK_EVENTS } from '@relayfile/adapter-linear/types'; /** * Known event names per Relayfile provider, used by the deploy CLI to lint @@ -24,6 +25,7 @@ const KNOWN_TRIGGER_ALIAS_CATALOG = Object.fromEntries( export const KNOWN_TRIGGERS = { ...KNOWN_TRIGGER_CATALOG, + linear: mergeKnownTriggers(KNOWN_TRIGGER_CATALOG.linear, LINEAR_AGENT_WEBHOOK_EVENTS), ...KNOWN_TRIGGER_ALIAS_CATALOG }; @@ -137,3 +139,10 @@ export function lintTriggers(agent: AgentSpec): TriggerLintIssue[] { function knownTriggersForProvider(provider: string): readonly string[] | undefined { return (KNOWN_TRIGGERS as Record)[provider]; } + +function mergeKnownTriggers( + base: readonly string[] | undefined, + extra: readonly string[] +): readonly string[] { + return [...new Set([...(base ?? []), ...extra])]; +} diff --git a/packages/runtime/src/define-agent.test.ts b/packages/runtime/src/define-agent.test.ts index a13b345f..ffea0157 100644 --- a/packages/runtime/src/define-agent.test.ts +++ b/packages/runtime/src/define-agent.test.ts @@ -1,7 +1,11 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import { defineAgent, isWorkforceAgent, isWorkforceHandler } from './index.js'; +import { defineAgent, isWorkforceAgent, isWorkforceHandler, unwrapResourceRecord } from './index.js'; +import type { + LinearAgentSessionPayload, + LinearAppUserNotificationPayload +} from './types.js'; test('defineAgent brands the object and wraps the handler', () => { const agent = defineAgent({ @@ -62,3 +66,80 @@ test('defineAgent narrows the handler event type to declared triggers/schedules' } }); }); + +test('defineAgent narrows Linear agent-session payloads for declared triggers', () => { + defineAgent({ + triggers: { + linear: [ + { on: 'AgentSessionEvent.created' }, + { on: 'AgentSessionEvent.prompted' }, + { on: 'AppUserNotification.issueCommentMention' } + ] + }, + handler: async (_ctx, event) => { + if (event.source !== 'linear') return; + if (event.type === 'AgentSessionEvent.created') { + const record = unwrapResourceRecord(event.payload); + if (!isLinearAgentSessionPayload(record)) return; + const sessionId: string = record.agentSession.id; + const promptContext: string | undefined = record.promptContext; + void sessionId; + void promptContext; + return; + } + if (event.type === 'AgentSessionEvent.prompted') { + const record = unwrapResourceRecord(event.payload); + if (!isLinearAgentSessionPayload(record)) return; + const body: string | undefined = record.agentActivity?.body; + void body; + return; + } + const record = unwrapResourceRecord(event.payload); + if (!isLinearAppUserNotificationPayload(record)) return; + const issueId: string | undefined = + record.notification?.issue?.id ?? + record.issue?.id; + const commentBody: string | undefined = + record.notification?.comment?.body ?? + record.comment?.body; + void issueId; + void commentBody; + } + }); +}); + +test('unwrapResourceRecord unwraps the real relayfile resource payload container', () => { + const record = unwrapResourceRecord<{ + body?: string; + issue_id?: string; + issue_identifier?: string; + }>({ + resource: { + provider: 'linear', + objectType: 'comment', + payload: { + body: '@agentrelay please implement this.', + issue_id: '5d6f2e15-0000-4000-8000-000000000000', + issue_identifier: 'AR-70' + } + } + }); + + assert.equal(isRecord(record), true); + if (!isRecord(record)) return; + assert.equal(record.body, '@agentrelay please implement this.'); + assert.equal(record.issue_id, '5d6f2e15-0000-4000-8000-000000000000'); + assert.equal(record.issue_identifier, 'AR-70'); +}); + +function isLinearAgentSessionPayload(value: unknown): value is LinearAgentSessionPayload { + return isRecord(value) && isRecord(value.agentSession) && typeof value.agentSession.id === 'string'; +} + +function isLinearAppUserNotificationPayload(value: unknown): value is LinearAppUserNotificationPayload { + return isRecord(value); +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} diff --git a/packages/runtime/src/define-agent.ts b/packages/runtime/src/define-agent.ts index da872f81..9a2a2185 100644 --- a/packages/runtime/src/define-agent.ts +++ b/packages/runtime/src/define-agent.ts @@ -10,6 +10,7 @@ import type { WorkforceEvent, WorkforceHandler, WorkforceHandlerExport, + LinearAgentSessionEvent, WorkforceProviderEvent } from './types.js'; @@ -35,9 +36,27 @@ type OnLiteralsOf = A extends readonly (infer E)[] type TriggerOnUnion = OnLiteralsOf>; +type LinearSpecialEvent = Extract; + +type ProviderEventFor

= P extends 'linear' + ? [LinearSpecialEvent] extends [never] + ? Omit & { type: O } + : LinearSpecialEvent + : Omit & { type: O }; + +type TriggerProviderEvents = { + [P in keyof Tr]: P extends string + ? OnLiteralsOf> extends infer O + ? O extends string + ? ProviderEventFor + : never + : never + : never; +}[keyof Tr]; + type NarrowedProviderEvent = [TriggerOnUnion] extends [never] ? never - : Omit & { type: TriggerOnUnion }; + : TriggerProviderEvents; /** Distributive union of every schedule `name` literal. */ type ScheduleNameUnion = S extends readonly (infer E)[] diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 56973229..4a37cda8 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -44,6 +44,23 @@ export type { WorkforceProviderEvent } from './types.js'; +// Runtime envelope helpers shared by provider-triggered agents. +export { + unwrapResourceRecord +} from './types.js'; + +export type { + LinearAgentActivity, + LinearAgentActivityType, + LinearAgentSession, + LinearAgentSessionEvent, + LinearAgentSessionEventPayload, + LinearAgentSessionPayload, + LinearAppUserNotificationEventPayload, + LinearAppUserNotificationPayload, + LinearIssueReference +} from './types.js'; + // VFS-backed transport helpers. All provider interactions go through these // — no per-provider client code in the runtime. Handlers use listJsonFiles / // readJsonFile / writeJsonFile directly against the provider path conventions diff --git a/packages/runtime/src/types.ts b/packages/runtime/src/types.ts index 1a12a8bc..07445f18 100644 --- a/packages/runtime/src/types.ts +++ b/packages/runtime/src/types.ts @@ -56,7 +56,112 @@ export interface WorkforceProviderEvent extends WorkforceEventBase { }; } -export type WorkforceEvent = WorkforceCronEvent | WorkforceProviderEvent; +export function unwrapResourceRecord(payload: unknown): T | unknown { + const resource = isRecord(payload) && 'resource' in payload ? payload.resource : payload; + return isRecord(resource) && 'payload' in resource ? resource.payload : resource; +} + +export interface LinearIssueReference { + id: string; + identifier?: string; + title?: string; + url?: string; +} + +export interface LinearAgentSession { + id: string; + issue?: LinearIssueReference; + [key: string]: unknown; +} + +export type LinearAgentActivityType = + | 'action' + | 'elicitation' + | 'error' + | 'response' + | 'thought'; + +export interface LinearAgentActivity { + type?: LinearAgentActivityType | string; + body?: string; + action?: string; + parameter?: string; + result?: string; +} + +export interface LinearAgentSessionPayload { + type: 'AgentSessionEvent'; + action: 'created' | 'prompted' | string; + agentSession: LinearAgentSession; + agentActivity?: LinearAgentActivity & { + id?: string; + agentSessionId?: string; + content?: LinearAgentActivity; + [key: string]: unknown; + }; + issue?: LinearIssueReference; + promptContext?: string; + notification?: { + issue?: LinearIssueReference; + comment?: { id?: string; body?: string; [key: string]: unknown }; + [key: string]: unknown; + }; + [key: string]: unknown; +} + +export interface LinearAppUserNotificationPayload { + type: 'AppUserNotification'; + action: 'issueCommentMention' | string; + issue?: LinearIssueReference; + comment?: { id?: string; body?: string; [key: string]: unknown }; + notification?: { + issue?: LinearIssueReference; + comment?: { id?: string; body?: string; [key: string]: unknown }; + [key: string]: unknown; + }; + [key: string]: unknown; +} + +export interface LinearWrappedPayload { + resource: { + payload: TRecord; + [key: string]: unknown; + }; + [key: string]: unknown; +} + +export type LinearAgentSessionEventPayload = + LinearWrappedPayload & { + agentSession?: LinearAgentSession; + agentActivity?: LinearAgentSessionPayload['agentActivity']; + issue?: LinearIssueReference; + promptContext?: string; + }; + +export type LinearAppUserNotificationEventPayload = + LinearWrappedPayload & { + issue?: LinearIssueReference; + comment?: { id?: string; body?: string; [key: string]: unknown }; + notification?: LinearAppUserNotificationPayload['notification']; + }; + +export type LinearAgentSessionEvent = + | (Omit & { + source: 'linear'; + type: 'AgentSessionEvent.created' | 'AgentSessionEvent.prompted'; + payload: LinearAgentSessionEventPayload; + }) + | (Omit & { + source: 'linear'; + type: 'AppUserNotification.issueCommentMention'; + payload: LinearAppUserNotificationEventPayload; + }); + +export type WorkforceEvent = WorkforceCronEvent | LinearAgentSessionEvent | WorkforceProviderEvent; + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} /** * Result of a harness invocation. The runtime translates whatever the diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d145752..c1fbf008 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: '@relayfile/adapter-core': specifier: ^0.3.26 version: 0.3.26(@relayfile/sdk@0.7.40) + '@relayfile/adapter-linear': + specifier: ^0.3.8 + version: 0.3.9(@relayfile/sdk@0.7.40) '@relayfile/local-mount': specifier: ^0.7.24 version: 0.7.24 @@ -860,6 +863,19 @@ packages: peerDependencies: '@relayfile/sdk': '>=0.6.0 <1' + '@relayfile/adapter-core@0.3.30': + resolution: {integrity: sha512-TW03JUsrb8D0lHFfsoP/H4YiglgRev+E+vZg7m2K7khDOtA5rfpU6LIcl9Wl9WrC22oKUrThzbh195yG2XAfyQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@relayfile/sdk': '>=0.6.0 <1' + + '@relayfile/adapter-linear@0.3.9': + resolution: {integrity: sha512-O3oTPieQNLdhE4nAnbj+WbUO0iMDlCEVO7OKJAzvrZruNjp1MDfmJ8s+ABR5AAEU7LhUzfMtGCnM2rCcP65kcA==} + engines: {node: '>=18'} + peerDependencies: + '@relayfile/sdk': '>=0.6.0 <1' + '@relayfile/core@0.7.40': resolution: {integrity: sha512-vY48SxZgahnvE0CHDyy/iny17ypnfbX5myVPtocZVNpMiz4dS1iabO70WR+uYVgSiIZR7MpKTPFSs3SxEjQWag==} engines: {node: '>=18'} @@ -3117,6 +3133,19 @@ snapshots: minimatch: 10.2.5 yaml: 2.9.0 + '@relayfile/adapter-core@0.3.30(@relayfile/sdk@0.7.40)': + dependencies: + '@relayfile/sdk': 0.7.40 + '@scalar/postman-to-openapi': 0.6.3 + cheerio: 1.2.0 + minimatch: 10.2.5 + yaml: 2.9.0 + + '@relayfile/adapter-linear@0.3.9(@relayfile/sdk@0.7.40)': + dependencies: + '@relayfile/adapter-core': 0.3.30(@relayfile/sdk@0.7.40) + '@relayfile/sdk': 0.7.40 + '@relayfile/core@0.7.40': {} '@relayfile/local-mount@0.7.24': From 54ff48dd767d6b5f09ea4230d37682d263683c59 Mon Sep 17 00:00:00 2001 From: Ricky Schema Cascade Date: Tue, 2 Jun 2026 22:09:43 +0200 Subject: [PATCH 2/2] chore: bump runtime packages for Linear event exports --- packages/persona-kit/package.json | 2 +- packages/runtime/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/persona-kit/package.json b/packages/persona-kit/package.json index 727de173..dfd7a8ee 100644 --- a/packages/persona-kit/package.json +++ b/packages/persona-kit/package.json @@ -1,6 +1,6 @@ { "name": "@agentworkforce/persona-kit", - "version": "3.0.40", + "version": "3.0.41", "private": false, "type": "module", "main": "dist/index.js", diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 3677d116..ae11f5f3 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@agentworkforce/runtime", - "version": "3.0.40", + "version": "3.0.41", "private": false, "type": "module", "main": "dist/index.js",