@finalai/final-agent-provider 0.6.2 → 0.7.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/HOSTING.md +4 -4
- package/README.md +94 -2
- package/dist/modules/authoring-final-channel.mjs +2 -2
- package/dist/modules/authoring-final-channel.mjs.map +2 -2
- package/dist/modules/channel-factories.d.mts +5 -7
- package/dist/modules/channel-factories.mjs +4 -50
- package/dist/modules/channel-factories.mjs.map +2 -2
- package/dist/modules/channels/channel-utils.d.mts +132 -0
- package/dist/modules/channels/channel-utils.mjs +445 -0
- package/dist/modules/channels/channel-utils.mjs.map +7 -0
- package/dist/modules/channels/delivery.d.mts +5 -5
- package/dist/modules/channels/delivery.mjs.map +1 -1
- package/dist/modules/channels/direct-messaging.d.mts +2 -2
- package/dist/modules/channels/direct-messaging.mjs.map +2 -2
- package/dist/modules/channels/discord-dispatch.d.mts +17 -0
- package/dist/modules/channels/discord-dispatch.mjs +139 -0
- package/dist/modules/channels/discord-dispatch.mjs.map +7 -0
- package/dist/modules/channels/discord-gateway.d.mts +19 -0
- package/dist/modules/channels/discord-gateway.mjs +233 -0
- package/dist/modules/channels/discord-gateway.mjs.map +7 -0
- package/dist/modules/channels/discord.d.mts +9 -0
- package/dist/modules/channels/discord.mjs +904 -0
- package/dist/modules/channels/discord.mjs.map +7 -0
- package/dist/modules/channels/fake-platform.d.mts +2 -7
- package/dist/modules/channels/fake-platform.mjs +2 -38
- package/dist/modules/channels/fake-platform.mjs.map +2 -2
- package/dist/modules/channels/github.d.mts +5 -0
- package/dist/modules/channels/github.mjs +720 -0
- package/dist/modules/channels/github.mjs.map +7 -0
- package/dist/modules/channels/group-messaging.d.mts +2 -2
- package/dist/modules/channels/group-messaging.mjs.map +2 -2
- package/dist/modules/channels/index.d.mts +7 -3
- package/dist/modules/channels/index.mjs +7 -3
- package/dist/modules/channels/index.mjs.map +2 -2
- package/dist/modules/channels/linear.d.mts +4 -0
- package/dist/modules/channels/linear.mjs +621 -0
- package/dist/modules/channels/linear.mjs.map +7 -0
- package/dist/modules/channels/messaging-channel.d.mts +3 -3
- package/dist/modules/channels/messaging-channel.mjs +2 -2
- package/dist/modules/channels/messaging-channel.mjs.map +2 -2
- package/dist/modules/channels/phone.d.mts +8 -0
- package/dist/modules/channels/phone.mjs +382 -0
- package/dist/modules/channels/phone.mjs.map +7 -0
- package/dist/modules/channels/projection.mjs.map +1 -1
- package/dist/modules/channels/reply-receipts.d.mts +31 -0
- package/dist/modules/channels/reply-receipts.mjs +63 -0
- package/dist/modules/channels/reply-receipts.mjs.map +7 -0
- package/dist/modules/channels/routing.d.mts +1 -0
- package/dist/modules/channels/routing.mjs +5 -1
- package/dist/modules/channels/routing.mjs.map +2 -2
- package/dist/modules/channels/slack.d.mts +7 -0
- package/dist/modules/channels/slack.mjs +1134 -0
- package/dist/modules/channels/slack.mjs.map +7 -0
- package/dist/modules/channels/voice-recording.d.mts +7 -0
- package/dist/modules/channels/voice-recording.mjs +49 -0
- package/dist/modules/channels/voice-recording.mjs.map +7 -0
- package/dist/modules/channels/voice.d.mts +33 -0
- package/dist/modules/channels/voice.mjs +851 -0
- package/dist/modules/channels/voice.mjs.map +7 -0
- package/dist/modules/channels/webhook.d.mts +5 -0
- package/dist/modules/channels/webhook.mjs +76 -0
- package/dist/modules/channels/webhook.mjs.map +7 -0
- package/dist/modules/harnesses/acp-describe.mjs +1 -1
- package/dist/modules/harnesses/acp-describe.mjs.map +2 -2
- package/dist/modules/harnesses/config-fields.d.mts +6 -1
- package/dist/modules/harnesses/config-fields.mjs +7 -0
- package/dist/modules/harnesses/config-fields.mjs.map +3 -3
- package/dist/modules/harnesses/fidelity.mjs +17 -17
- package/dist/modules/harnesses/fidelity.mjs.map +2 -2
- package/dist/modules/harnesses/remote-claude-code.d.mts +14 -2
- package/dist/modules/harnesses/remote-claude-code.mjs +5 -3
- package/dist/modules/harnesses/remote-claude-code.mjs.map +2 -2
- package/dist/modules/harnesses/remote-url.mjs.map +1 -1
- package/dist/modules/harnesses/serverless-nanocode-codex.d.mts +3 -1
- package/dist/modules/harnesses/serverless-nanocode-codex.mjs +6 -0
- package/dist/modules/harnesses/serverless-nanocode-codex.mjs.map +2 -2
- package/dist/modules/runtime/remote-host.mjs.map +1 -1
- package/dist/modules/sandboxes/build-scripts.mjs.map +1 -1
- package/dist/modules/sandboxes/paths.mjs.map +1 -1
- package/dist/modules/testing/harness.mjs +2 -2
- package/dist/modules/testing/harness.mjs.map +1 -1
- package/package.json +45 -19
- package/dist/modules/channels/address-context.d.mts +0 -7
- package/dist/modules/channels/address-context.mjs +0 -80
- package/dist/modules/channels/address-context.mjs.map +0 -7
- package/dist/modules/channels/agent-events.d.mts +0 -103
- package/dist/modules/channels/agent-events.mjs +0 -341
- package/dist/modules/channels/agent-events.mjs.map +0 -7
- package/dist/modules/channels/agent-schedules.d.mts +0 -25
- package/dist/modules/channels/agent-schedules.mjs +0 -96
- package/dist/modules/channels/agent-schedules.mjs.map +0 -7
- package/dist/modules/channels/service-input.d.mts +0 -22
- package/dist/modules/channels/service-input.mjs +0 -32
- package/dist/modules/channels/service-input.mjs.map +0 -7
|
@@ -0,0 +1,621 @@
|
|
|
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
|