@finalai/final-agent-provider 0.8.0 → 0.10.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.
Files changed (53) hide show
  1. package/HOSTING.md +3 -3
  2. package/README.md +7 -91
  3. package/dist/modules/channel-factories.d.mts +5 -6
  4. package/dist/modules/channel-factories.mjs +7 -11
  5. package/dist/modules/channel-factories.mjs.map +3 -3
  6. package/dist/modules/channels/inbound.mjs +2 -2
  7. package/dist/modules/channels/inbound.mjs.map +2 -2
  8. package/dist/modules/channels/index.d.mts +0 -7
  9. package/dist/modules/channels/index.mjs +0 -7
  10. package/dist/modules/channels/index.mjs.map +2 -2
  11. package/dist/modules/channels/prompt.d.mts +2 -2
  12. package/dist/modules/channels/prompt.mjs +3 -3
  13. package/dist/modules/channels/prompt.mjs.map +2 -2
  14. package/dist/modules/channels/routing.d.mts +1 -1
  15. package/dist/modules/channels/routing.mjs +1 -1
  16. package/dist/modules/channels/routing.mjs.map +1 -1
  17. package/package.json +4 -40
  18. package/dist/modules/channels/channel-utils.d.mts +0 -132
  19. package/dist/modules/channels/channel-utils.mjs +0 -445
  20. package/dist/modules/channels/channel-utils.mjs.map +0 -7
  21. package/dist/modules/channels/discord-dispatch.d.mts +0 -17
  22. package/dist/modules/channels/discord-dispatch.mjs +0 -139
  23. package/dist/modules/channels/discord-dispatch.mjs.map +0 -7
  24. package/dist/modules/channels/discord-gateway.d.mts +0 -19
  25. package/dist/modules/channels/discord-gateway.mjs +0 -233
  26. package/dist/modules/channels/discord-gateway.mjs.map +0 -7
  27. package/dist/modules/channels/discord.d.mts +0 -9
  28. package/dist/modules/channels/discord.mjs +0 -904
  29. package/dist/modules/channels/discord.mjs.map +0 -7
  30. package/dist/modules/channels/github.d.mts +0 -5
  31. package/dist/modules/channels/github.mjs +0 -720
  32. package/dist/modules/channels/github.mjs.map +0 -7
  33. package/dist/modules/channels/linear.d.mts +0 -4
  34. package/dist/modules/channels/linear.mjs +0 -621
  35. package/dist/modules/channels/linear.mjs.map +0 -7
  36. package/dist/modules/channels/phone.d.mts +0 -8
  37. package/dist/modules/channels/phone.mjs +0 -382
  38. package/dist/modules/channels/phone.mjs.map +0 -7
  39. package/dist/modules/channels/reply-receipts.d.mts +0 -31
  40. package/dist/modules/channels/reply-receipts.mjs +0 -63
  41. package/dist/modules/channels/reply-receipts.mjs.map +0 -7
  42. package/dist/modules/channels/slack.d.mts +0 -7
  43. package/dist/modules/channels/slack.mjs +0 -1134
  44. package/dist/modules/channels/slack.mjs.map +0 -7
  45. package/dist/modules/channels/voice-recording.d.mts +0 -7
  46. package/dist/modules/channels/voice-recording.mjs +0 -49
  47. package/dist/modules/channels/voice-recording.mjs.map +0 -7
  48. package/dist/modules/channels/voice.d.mts +0 -33
  49. package/dist/modules/channels/voice.mjs +0 -851
  50. package/dist/modules/channels/voice.mjs.map +0 -7
  51. package/dist/modules/channels/webhook.d.mts +0 -5
  52. package/dist/modules/channels/webhook.mjs +0 -76
  53. package/dist/modules/channels/webhook.mjs.map +0 -7
@@ -1,621 +0,0 @@
1
- import {
2
- defineChannel,
3
- packageRef,
4
- jsonObjectSchema,
5
- jsonValueSchema
6
- } from "@finalai/agent-provider";
7
- import * as z from "zod";
8
- import { shareReplyReceipt } from "./reply-receipts.mjs";
9
- import {
10
- channelBaseUrl,
11
- providerPlaceKey,
12
- confirmOAuth,
13
- deliveryRetention,
14
- decisionChoices,
15
- decisionAnswer,
16
- decisionSummary,
17
- externalKey,
18
- inTenant,
19
- providerOriginSchema,
20
- parseJson,
21
- personFor,
22
- receivePersonMessage,
23
- refString,
24
- responseJson,
25
- startOAuth,
26
- stateKey,
27
- takeOAuth,
28
- textOf,
29
- completionText,
30
- verifyHmac
31
- } from "./channel-utils.mjs";
32
- const configSchema = z.object({ baseUrl: z.string().url().optional() });
33
- const pendingDecisionsSchema = z.array(
34
- z.object({
35
- decisionId: z.string(),
36
- user: z.string(),
37
- choices: z.array(
38
- z.object({
39
- label: z.string(),
40
- outcome: jsonValueSchema,
41
- answer: z.union([z.literal("approve"), z.literal("deny")])
42
- })
43
- )
44
- })
45
- );
46
- const graphql = async (ctx, query, variables, token) => {
47
- const accessToken = token ?? (await ctx.credentials.app({ tenant: ctx.origin?.place.tenant })).accessToken;
48
- const response = z.object({
49
- data: jsonObjectSchema.optional(),
50
- errors: z.array(z.object({ message: z.string() })).optional()
51
- }).parse(
52
- await responseJson(
53
- await ctx.credentials.fetch("https://api.linear.app/graphql", {
54
- method: "POST",
55
- headers: { authorization: `Bearer ${accessToken}`, "content-type": "application/json" },
56
- body: JSON.stringify({ query, variables })
57
- })
58
- )
59
- );
60
- if (response.errors != null || response.data == null)
61
- throw new Error(
62
- `Linear GraphQL: ${response.errors?.map((error) => error.message).join("; ") ?? "missing data"}`
63
- );
64
- return response.data;
65
- };
66
- const createActivity = async (ctx, sessionId, content, key) => {
67
- if (content.type === "response" && (typeof content.body !== "string" || content.body.trim().length === 0))
68
- return;
69
- if (key != null && await ctx.store.get(key) != null) return;
70
- const data = await graphql(
71
- ctx,
72
- "mutation ChannelActivity($input: AgentActivityCreateInput!) { agentActivityCreate(input: $input) { success agentActivity { id } } }",
73
- { input: { agentSessionId: sessionId, content } }
74
- );
75
- const result = z.object({
76
- agentActivityCreate: z.object({
77
- success: z.boolean(),
78
- agentActivity: z.object({ id: z.string() }).nullish()
79
- })
80
- }).parse(data).agentActivityCreate;
81
- if (!result.success || result.agentActivity == null) throw new Error("Linear activity rejected");
82
- if (key != null) await ctx.store.set(key, result.agentActivity.id, deliveryRetention);
83
- };
84
- const activitySession = async (ctx) => {
85
- const savedSession = z.string().safeParse(
86
- await ctx.store.get(stateKey("linear-session-for-place", providerPlaceKey(ctx.origin.place)))
87
- );
88
- return refString(ctx.origin.ref, "sessionId") ?? (ctx.origin.place.kind === "agent-session" ? ctx.origin.place.id : savedSession.success ? savedSession.data : null);
89
- };
90
- const activity = async (ctx, content, key) => {
91
- const sessionId = await activitySession(ctx);
92
- if (sessionId != null) await createActivity(ctx, sessionId, content, key);
93
- };
94
- const sessionSchema = z.object({
95
- type: z.literal("AgentSessionEvent"),
96
- action: z.string(),
97
- organizationId: z.string(),
98
- webhookTimestamp: z.number(),
99
- agentSession: z.object({
100
- id: z.string(),
101
- creatorId: z.string().nullish(),
102
- creator: z.object({ id: z.string(), name: z.string() }).nullish(),
103
- issue: z.object({ id: z.string(), title: z.string(), description: z.string().nullish() }).nullish(),
104
- comment: z.object({ id: z.string(), body: z.string(), userId: z.string().optional() }).nullish()
105
- }),
106
- agentActivity: z.object({
107
- id: z.string(),
108
- userId: z.string(),
109
- user: z.object({ id: z.string(), name: z.string() }),
110
- content: jsonObjectSchema,
111
- signal: z.string().nullish(),
112
- sourceCommentId: z.string().nullish()
113
- }).nullish()
114
- });
115
- const linearChannel = (options = {}) => {
116
- const config = configSchema.parse(options);
117
- return defineChannel({
118
- name: "linear",
119
- config,
120
- installed: packageRef("@finalai/final-agent-provider/channels/linear"),
121
- credentials: { app: "linear" },
122
- identity: {
123
- kind: "redirect",
124
- start: async (args, ctx) => {
125
- const app = await ctx.credentials.appInfo();
126
- return startOAuth(
127
- {
128
- ...args,
129
- authorizeUrl: "https://linear.app/oauth/authorize",
130
- clientId: app.clientId,
131
- redirectUri: `${channelBaseUrl(config)}/b/${ctx.binding.id}/oauth/callback`,
132
- scope: "read",
133
- extra: { actor: "user" }
134
- },
135
- ctx
136
- );
137
- }
138
- },
139
- places: {
140
- kinds: ["agent-session", "issue"],
141
- defaults: (place) => place.kind === "agent-session" || place.kind === "issue" ? { create: "group", replies: "origin", audience: "members" } : null
142
- },
143
- routes: [
144
- {
145
- method: "GET",
146
- path: "/oauth/callback",
147
- handler: async (ctx) => {
148
- const saved = await takeOAuth(ctx.request, ctx);
149
- if (saved == null)
150
- return new Response("Invalid or expired link request", { status: 400 });
151
- const app = await ctx.credentials.appInfo();
152
- const token = z.object({ access_token: z.string() }).parse(
153
- await responseJson(
154
- await ctx.credentials.fetch("https://api.linear.app/oauth/token", {
155
- method: "POST",
156
- body: new URLSearchParams({
157
- client_id: app.clientId,
158
- client_secret: app.clientSecret,
159
- grant_type: "authorization_code",
160
- code: saved.code,
161
- redirect_uri: saved.redirectUri
162
- })
163
- })
164
- )
165
- );
166
- const user = z.object({
167
- viewer: z.object({
168
- id: z.string(),
169
- name: z.string(),
170
- organization: z.object({ id: z.string() })
171
- })
172
- }).parse(
173
- await graphql(
174
- ctx,
175
- "query ChannelIdentity { viewer { id name organization { id } } }",
176
- {},
177
- token.access_token
178
- )
179
- ).viewer;
180
- return confirmOAuth(
181
- {
182
- ticket: saved.ticket,
183
- expected: saved.principal,
184
- actual: { id: user.id, tenant: user.organization.id, displayName: user.name }
185
- },
186
- ctx
187
- );
188
- }
189
- },
190
- {
191
- method: "POST",
192
- path: "/webhook",
193
- handler: async (ctx) => {
194
- const body = await ctx.request.text();
195
- const app = await ctx.credentials.appInfo();
196
- const secret = app.signingSecret;
197
- const signature = ctx.request.headers.get("linear-signature");
198
- if (secret == null || signature == null || !await verifyHmac({ secret, signature, body }))
199
- return new Response(null, { status: 401 });
200
- const raw = jsonValueSchema.parse(parseJson(body));
201
- const timestamp = z.object({ webhookTimestamp: z.number() }).safeParse(raw);
202
- if (!timestamp.success || Math.abs(Date.now() - timestamp.data.webhookTimestamp) > 3e5)
203
- return new Response(null, { status: 401 });
204
- await ctx.receive(raw);
205
- return new Response("ok");
206
- }
207
- }
208
- ],
209
- receive: async (input, baseContext) => {
210
- const tenant = z.object({ organizationId: z.string() }).safeParse(input);
211
- const ctx = tenant.success ? inTenant(baseContext, tenant.data.organizationId) : baseContext;
212
- const lifecycle = z.object({ type: z.string(), action: z.string() }).safeParse(input);
213
- if (lifecycle.success && lifecycle.data.type === "OAuthApp" && lifecycle.data.action === "revoked") {
214
- if (!tenant.success) return { ignored: true, reason: "missing-tenant" };
215
- await ctx.people.revoke({ tenant: tenant.data.organizationId, reason: "uninstalled" });
216
- return { accepted: true };
217
- }
218
- const mutation = z.object({
219
- type: z.string(),
220
- action: z.string(),
221
- actor: z.object({ id: z.string() }).optional(),
222
- data: z.object({
223
- id: z.string(),
224
- body: z.string().optional(),
225
- userId: z.string().optional(),
226
- commentId: z.string().optional(),
227
- emoji: z.string().optional()
228
- })
229
- }).safeParse(input);
230
- if (mutation.success && (mutation.data.type === "Comment" || mutation.data.type === "Reaction")) {
231
- const event2 = mutation.data;
232
- const saved = providerOriginSchema.safeParse(
233
- await ctx.store.get(stateKey("linear-origin", event2.data.commentId ?? event2.data.id))
234
- );
235
- if (!saved.success) return { ignored: true, reason: "unknown-origin" };
236
- const userId = event2.actor?.id ?? event2.data.userId;
237
- if (userId == null || !tenant.success) return { ignored: true, reason: "missing-actor" };
238
- const by2 = await personFor(ctx, { id: userId, tenant: tenant.data.organizationId });
239
- if (typeof by2 !== "string") return { accepted: false, refusal: by2 };
240
- const route = await ctx.places.resolve(saved.data.place);
241
- if (route.kind !== "connected") return { ignored: true, reason: "unconnected" };
242
- const conversation = ctx.conversation(route.conversation);
243
- if (event2.type === "Comment" && event2.action === "update" && event2.data.body != null) {
244
- const result2 = await conversation.edit({
245
- externalKey: saved.data.externalKey,
246
- by: by2,
247
- content: [{ type: "text", text: event2.data.body }]
248
- });
249
- return "refused" in result2 ? { accepted: false, refusal: result2 } : { accepted: true };
250
- }
251
- if (event2.type === "Comment" && event2.action === "remove") {
252
- const result2 = await conversation.remove({ externalKey: saved.data.externalKey, by: by2 });
253
- return "refused" in result2 ? { accepted: false, refusal: result2 } : { accepted: true };
254
- }
255
- if (event2.type === "Reaction" && event2.data.emoji != null) {
256
- const result2 = await conversation.react({
257
- target: { externalKey: saved.data.externalKey },
258
- by: by2,
259
- emoji: event2.data.emoji,
260
- op: event2.action === "remove" ? "remove" : "add"
261
- });
262
- return "refused" in result2 ? { accepted: false, refusal: result2 } : { accepted: true };
263
- }
264
- return { ignored: true, reason: "unsupported-mutation" };
265
- }
266
- const parsed = sessionSchema.safeParse(input);
267
- if (!parsed.success) return { ignored: true, reason: "not-agent-session" };
268
- const event = parsed.data;
269
- if (event.action !== "created" && event.action !== "prompted")
270
- return { ignored: true, reason: "unsupported-action" };
271
- const session = event.agentSession;
272
- const user = event.agentActivity?.userId ?? session.creatorId;
273
- if (user == null) return { ignored: true, reason: "no-human-requester" };
274
- const principal = {
275
- id: user,
276
- tenant: event.organizationId,
277
- displayName: event.agentActivity?.user.name ?? session.creator?.name ?? user
278
- };
279
- const content = event.agentActivity?.content.body;
280
- const text = typeof content === "string" ? content : session.comment?.body ?? session.issue?.title ?? "Please help with this issue.";
281
- const by = await personFor(ctx, principal);
282
- const routePlace = session.issue == null ? { kind: "agent-session", id: session.id, tenant: event.organizationId } : {
283
- kind: "issue",
284
- id: session.issue.id,
285
- tenant: event.organizationId,
286
- title: session.issue.title
287
- };
288
- const place = session.issue == null ? routePlace : {
289
- kind: "agent-session",
290
- id: session.id,
291
- tenant: event.organizationId,
292
- title: session.issue.title,
293
- parent: routePlace
294
- };
295
- if (event.agentActivity?.signal === "stop") {
296
- const activityId = event.agentActivity.id;
297
- const refuseStop = async (reason) => {
298
- await createActivity(
299
- ctx,
300
- session.id,
301
- {
302
- type: "error",
303
- body: "Could not stop this run from this account. Open Final to manage it."
304
- },
305
- stateKey("linear-stop-refused", session.id, activityId)
306
- );
307
- return { accepted: false, refusal: reason };
308
- };
309
- const route = await ctx.places.resolve(routePlace);
310
- if (route.kind !== "connected")
311
- return refuseStop({
312
- refused: "unconnected",
313
- message: "This place is not connected to Final.",
314
- connectUrl: route.connectUrl
315
- });
316
- if (typeof by !== "string" && !(by.refused === "unlinked" && route.policy.audience === "anyone" && session.creatorId === user))
317
- return refuseStop(by);
318
- const key2 = stateKey(
319
- "linear-stop",
320
- providerPlaceKey(place),
321
- session.id,
322
- event.agentActivity.id
323
- );
324
- const saved = z.string().safeParse(await ctx.store.get(key2));
325
- let message;
326
- if (saved.success) message = saved.data;
327
- else {
328
- const target = z.object({ turnId: z.string() }).safeParse(await ctx.store.get(stateKey("linear-session-turn", session.id)));
329
- if (!target.success)
330
- message = "There is no accepted turn in this session to stop. Open Final to manage pending work.";
331
- else {
332
- const cancelled = await ctx.turns.cancel({
333
- conversation: route.conversation,
334
- by: typeof by === "string" ? by : void 0,
335
- turnId: target.data.turnId
336
- });
337
- if ("refused" in cancelled) return refuseStop(cancelled);
338
- message = cancelled.accepted ? "The agent has stopped." : "There is no active turn this account may stop. Continue in Final.";
339
- }
340
- await ctx.store.set(key2, message, deliveryRetention);
341
- }
342
- await createActivity(
343
- ctx,
344
- session.id,
345
- { type: "response", body: message },
346
- stateKey("linear-stop-confirmed", session.id, event.agentActivity.id)
347
- );
348
- return { accepted: true };
349
- }
350
- const answerKey = event.action === "prompted" && event.agentActivity != null ? stateKey("linear-answer-activity", session.id, event.agentActivity.id) : null;
351
- const replay = z.object({ decisionId: z.string(), outcome: jsonValueSchema }).safeParse(answerKey == null ? null : await ctx.store.get(answerKey));
352
- if (replay.success) {
353
- if (typeof by !== "string") return { accepted: false, refusal: by };
354
- const result2 = await ctx.decisions.resolve({ ...replay.data, by });
355
- return "refused" in result2 ? { accepted: false, refusal: result2 } : { accepted: true };
356
- }
357
- const pending = pendingDecisionsSchema.safeParse(
358
- await ctx.store.get(stateKey("linear-decision", session.id)) ?? []
359
- );
360
- const owned = pending.success ? pending.data.filter((entry) => entry.user === user) : [];
361
- if (event.action === "prompted" && owned.length > 0 && typeof by === "string") {
362
- const normalized = text.trim().toLowerCase().replace(/[.!?]$/g, "");
363
- const answer = ["yes", "no", "approve", "deny"].includes(normalized);
364
- if (pending.success && pending.data.length > 1) {
365
- if (answer || owned.some((entry) => decisionAnswer(entry.choices, text) != null)) {
366
- await createActivity(ctx, session.id, {
367
- type: "response",
368
- body: "Multiple actions need approval. Answer them in Final."
369
- });
370
- return { accepted: true };
371
- }
372
- } else {
373
- const current = owned[0];
374
- const choice = current == null ? null : decisionAnswer(current.choices, text);
375
- if (current != null && choice != null) {
376
- if (answerKey != null)
377
- await ctx.store.set(
378
- answerKey,
379
- { decisionId: current.decisionId, outcome: choice.outcome },
380
- deliveryRetention
381
- );
382
- const result2 = await ctx.decisions.resolve({
383
- decisionId: current.decisionId,
384
- outcome: choice.outcome,
385
- by
386
- });
387
- if ("refused" in result2) {
388
- await createActivity(ctx, session.id, { type: "error", body: result2.message });
389
- return { accepted: false, refusal: result2 };
390
- }
391
- const latest = pendingDecisionsSchema.safeParse(
392
- await ctx.store.get(stateKey("linear-decision", session.id)) ?? []
393
- );
394
- if (latest.success) {
395
- const remaining = latest.data.filter(
396
- (entry) => entry.decisionId !== current.decisionId
397
- );
398
- await ctx.store.set(
399
- stateKey("linear-decision", session.id),
400
- remaining.length === 0 ? null : jsonValueSchema.parse(remaining),
401
- { ttlSeconds: 3600 }
402
- );
403
- }
404
- if (!result2.resolved) {
405
- const message = "This decision is no longer waiting in Final.";
406
- await createActivity(ctx, session.id, { type: "error", body: message });
407
- return { accepted: false, refusal: { refused: "no-access", message } };
408
- }
409
- return { accepted: true };
410
- }
411
- if (answer) {
412
- await createActivity(ctx, session.id, {
413
- type: "response",
414
- body: "Choose the exact approval option in Final."
415
- });
416
- return { accepted: true };
417
- }
418
- }
419
- }
420
- await createActivity(
421
- ctx,
422
- session.id,
423
- { type: "thought", body: "I\u2019m looking into this." },
424
- stateKey("linear-ack", session.id, event.agentActivity?.id ?? "created")
425
- );
426
- await ctx.store.set(
427
- stateKey("linear-session-for-place", providerPlaceKey(routePlace)),
428
- session.id,
429
- deliveryRetention
430
- );
431
- await ctx.store.set(
432
- stateKey("linear-session-for-place", providerPlaceKey(place)),
433
- session.id,
434
- deliveryRetention
435
- );
436
- const key = externalKey(place, event.agentActivity?.id ?? session.comment?.id ?? session.id);
437
- const sourceComment = event.agentActivity?.sourceCommentId ?? (event.agentActivity == null ? session.comment?.id : void 0);
438
- if (sourceComment != null)
439
- await ctx.store.set(
440
- stateKey("linear-origin", sourceComment),
441
- jsonValueSchema.parse({ place, externalKey: key }),
442
- deliveryRetention
443
- );
444
- const result = await receivePersonMessage({
445
- ctx,
446
- principal,
447
- place,
448
- routePlace,
449
- onAccepted: async ({ turnId }) => {
450
- if (turnId != null)
451
- await ctx.store.set(
452
- stateKey("linear-session-turn", session.id),
453
- { turnId },
454
- deliveryRetention
455
- );
456
- },
457
- text,
458
- externalKey: key,
459
- context: session.issue == null ? [] : [
460
- `Issue: ${session.issue.title}`,
461
- ...session.issue.description == null ? [] : [session.issue.description]
462
- ],
463
- ref: {
464
- externalKey: key,
465
- sessionId: session.id,
466
- requester: user,
467
- tenant: event.organizationId
468
- }
469
- });
470
- if ("accepted" in result && !result.accepted) {
471
- await createActivity(
472
- ctx,
473
- session.id,
474
- {
475
- type: "response",
476
- body: `${result.refusal.message} ${result.refusal.linkUrl ?? result.refusal.connectUrl ?? ""}`
477
- },
478
- stateKey("linear-refusal", key)
479
- );
480
- await createActivity(
481
- ctx,
482
- session.id,
483
- {
484
- type: "error",
485
- body: "Open Final to resolve this request, then mention the agent again."
486
- },
487
- stateKey("linear-refusal-error", key)
488
- );
489
- }
490
- return result;
491
- },
492
- events: {
493
- "turn.started": async (data, ctx) => {
494
- const sessionId = refString(ctx.origin.ref, "sessionId") ?? (ctx.origin.place.kind === "agent-session" ? ctx.origin.place.id : null);
495
- if (sessionId != null)
496
- await ctx.store.set(
497
- stateKey("linear-session-turn", sessionId),
498
- { turnId: data.turnId },
499
- deliveryRetention
500
- );
501
- },
502
- "message.posted": async (data, ctx) => activity(
503
- ctx,
504
- {
505
- type: "response",
506
- body: `From ${data.author.displayName} in Final
507
-
508
- ${textOf(data.message.content)}`
509
- },
510
- stateKey("linear-mirror", providerPlaceKey(ctx.origin.place), data.message.id)
511
- ),
512
- "turn.completed": async (data, ctx) => {
513
- const sessionId = await activitySession(ctx);
514
- if (sessionId == null) return;
515
- await shareReplyReceipt({
516
- bindingId: ctx.binding.id,
517
- store: ctx.store,
518
- destination: stateKey("linear", ctx.origin.place.tenant ?? null, sessionId),
519
- messageIds: data.messages?.map((message) => message.id) ?? [],
520
- originKey: stateKey("linear-response", providerPlaceKey(ctx.origin.place), ctx.turn.id),
521
- send: (key) => createActivity(
522
- ctx,
523
- sessionId,
524
- {
525
- type: data.stopReason === "error" || data.stopReason === "timed_out" ? "error" : "response",
526
- body: completionText(data)
527
- },
528
- key
529
- )
530
- });
531
- },
532
- "tool.started": async (data, ctx) => activity(
533
- ctx,
534
- { type: "action", action: data.title, parameter: "In progress" },
535
- stateKey(
536
- "linear-tool",
537
- providerPlaceKey(ctx.origin.place),
538
- ctx.turn.id,
539
- data.toolCallId,
540
- "started"
541
- )
542
- ),
543
- "tool.completed": async (data, ctx) => activity(
544
- ctx,
545
- {
546
- type: "action",
547
- action: data.title,
548
- parameter: data.status,
549
- result: data.content == null ? data.status : textOf(data.content)
550
- },
551
- stateKey(
552
- "linear-tool",
553
- providerPlaceKey(ctx.origin.place),
554
- ctx.turn.id,
555
- data.toolCallId,
556
- "completed"
557
- )
558
- ),
559
- "plan.updated": async (data, ctx) => {
560
- const sessionId = refString(ctx.origin.ref, "sessionId") ?? (ctx.origin.place.kind === "agent-session" ? ctx.origin.place.id : null);
561
- if (sessionId == null) return;
562
- const plan = data.entries.flatMap(
563
- (entry) => typeof entry.content === "string" ? [
564
- {
565
- content: entry.content,
566
- status: entry.status === "completed" ? "completed" : entry.status === "in_progress" || entry.status === "inProgress" ? "inProgress" : entry.status === "canceled" ? "canceled" : "pending"
567
- }
568
- ] : []
569
- );
570
- await graphql(
571
- ctx,
572
- "mutation ChannelPlan($id: String!, $input: AgentSessionUpdateInput!) { agentSessionUpdate(id: $id, input: $input) { success } }",
573
- { id: sessionId, input: { plan } }
574
- );
575
- },
576
- "decision.resolved": async (data, ctx) => {
577
- const sessionId = refString(ctx.origin.ref, "sessionId") ?? (ctx.origin.place.kind === "agent-session" ? ctx.origin.place.id : null);
578
- if (sessionId == null) return;
579
- const pending = pendingDecisionsSchema.safeParse(
580
- await ctx.store.get(stateKey("linear-decision", sessionId)) ?? []
581
- );
582
- if (pending.success) {
583
- const remaining = pending.data.filter((entry) => entry.decisionId !== data.decisionId);
584
- await ctx.store.set(
585
- stateKey("linear-decision", sessionId),
586
- remaining.length === 0 ? null : jsonValueSchema.parse(remaining),
587
- { ttlSeconds: 3600 }
588
- );
589
- }
590
- },
591
- "decision.created": async (data, ctx) => {
592
- const sessionId = refString(ctx.origin.ref, "sessionId") ?? (ctx.origin.place.kind === "agent-session" ? ctx.origin.place.id : null);
593
- const user = refString(ctx.origin.ref, "requester");
594
- if (sessionId == null || user == null) return;
595
- const choices = decisionChoices(data.request);
596
- const pending = pendingDecisionsSchema.safeParse(
597
- await ctx.store.get(stateKey("linear-decision", sessionId)) ?? []
598
- );
599
- const existing = pending.success ? pending.data.filter((entry) => entry.decisionId !== data.decisionId) : [];
600
- await ctx.store.set(
601
- stateKey("linear-decision", sessionId),
602
- jsonValueSchema.parse([...existing, { decisionId: data.decisionId, user, choices }]),
603
- { ttlSeconds: 3600 }
604
- );
605
- await activity(
606
- ctx,
607
- {
608
- type: "elicitation",
609
- body: `${decisionSummary(data.request)}
610
- ${choices.length === 0 ? "Answer in Final \u2192" : choices.map((choice) => choice.label).join(" / ")}`
611
- },
612
- stateKey("linear-elicitation", providerPlaceKey(ctx.origin.place), data.decisionId)
613
- );
614
- }
615
- }
616
- });
617
- };
618
- export {
619
- linearChannel
620
- };
621
- //# sourceMappingURL=linear.mjs.map