create-loopengine 0.1.62 → 0.1.64
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/package.json +1 -1
- package/template/_gitignore +2 -0
- package/template/adapters/http.ts +85 -4
- package/template/package.json +1 -1
package/package.json
CHANGED
package/template/_gitignore
CHANGED
|
@@ -129,6 +129,10 @@ import {
|
|
|
129
129
|
replyInSlack,
|
|
130
130
|
slackOutcomeText,
|
|
131
131
|
verifySlackRequest,
|
|
132
|
+
firstSighting,
|
|
133
|
+
handleInboundMessage,
|
|
134
|
+
createSlackConnector,
|
|
135
|
+
parseSlackEvent,
|
|
132
136
|
removeAgentNotifier,
|
|
133
137
|
saveAgentNotifier,
|
|
134
138
|
testAgentNotifier,
|
|
@@ -2350,6 +2354,10 @@ async function answerPendingQuestion(res: ServerResponse, pendingId: string, bod
|
|
|
2350
2354
|
* resumeAgent(), through the same durable sessions.withSession append
|
|
2351
2355
|
* path any other turn goes through. */
|
|
2352
2356
|
async function respondAfterResolution(res: ServerResponse, finalCheckpoint: TurnCheckpoint): Promise<void> {
|
|
2357
|
+
await tracked(() => respondAfterResolutionNow(res, finalCheckpoint))
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
async function respondAfterResolutionNow(res: ServerResponse, finalCheckpoint: TurnCheckpoint): Promise<void> {
|
|
2353
2361
|
if (Object.keys(finalCheckpoint.outstanding).length > 0) {
|
|
2354
2362
|
res.writeHead(200, { 'content-type': 'application/json' }).end(
|
|
2355
2363
|
JSON.stringify({ resolved: true, outstanding: Object.keys(finalCheckpoint.outstanding).length }),
|
|
@@ -2584,11 +2592,24 @@ function exitIfCodeChanged(res: ServerResponse): void {
|
|
|
2584
2592
|
// checks before stopping an idle server, so it never stops one mid-turn.
|
|
2585
2593
|
let openStreams = 0
|
|
2586
2594
|
let lastRequestAt = Date.now()
|
|
2595
|
+
// Work still running after its request was answered (a Slack thread reply
|
|
2596
|
+
// being answered, a run resumed after a decision), so a host never
|
|
2597
|
+
// suspends this server in the middle of it.
|
|
2598
|
+
let backgroundWork = 0
|
|
2599
|
+
|
|
2600
|
+
async function tracked<T>(fn: () => Promise<T>): Promise<T> {
|
|
2601
|
+
backgroundWork++
|
|
2602
|
+
try {
|
|
2603
|
+
return await fn()
|
|
2604
|
+
} finally {
|
|
2605
|
+
backgroundWork--
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2587
2608
|
|
|
2588
2609
|
async function handleActivityGet(res: ServerResponse): Promise<void> {
|
|
2589
2610
|
res.writeHead(200, { 'content-type': 'application/json' }).end(
|
|
2590
2611
|
JSON.stringify({
|
|
2591
|
-
inFlightTurns: sessionTurns.size + scheduler.running,
|
|
2612
|
+
inFlightTurns: sessionTurns.size + scheduler.running + backgroundWork,
|
|
2592
2613
|
scheduledRuns: scheduler.running,
|
|
2593
2614
|
// When the next scheduled task is due — wake this server by then
|
|
2594
2615
|
// (POST /internal/scheduler/tick) if it's been stopped while idle.
|
|
@@ -2666,7 +2687,7 @@ function captureResponse(): { res: ServerResponse; done: Promise<{ status: numbe
|
|
|
2666
2687
|
return { res: res as unknown as ServerResponse, done }
|
|
2667
2688
|
}
|
|
2668
2689
|
|
|
2669
|
-
// A Slack app's Interactivity Request URL (core/slack
|
|
2690
|
+
// A Slack app's Interactivity Request URL (core/messaging/connectors/slack/interactions.ts):
|
|
2670
2691
|
// Approve/Deny, an answer option, or the "Other…" modal, from a Slack
|
|
2671
2692
|
// notifier's message. Outside the admin login on purpose — Slack can't
|
|
2672
2693
|
// send it — and verified with that agent's Slack signing secret instead.
|
|
@@ -2705,6 +2726,52 @@ async function handleSlackInteraction(req: IncomingMessage, res: ServerResponse,
|
|
|
2705
2726
|
}
|
|
2706
2727
|
}
|
|
2707
2728
|
|
|
2729
|
+
// A Slack app's Event Subscriptions Request URL (core/messaging/):
|
|
2730
|
+
// a reply in the thread under an approval request or a question, which
|
|
2731
|
+
// the agent answers about that request. Outside the admin login like the
|
|
2732
|
+
// button endpoint above, and verified with the same signing secret.
|
|
2733
|
+
// Slack's URL check is answered before that, since the app is created
|
|
2734
|
+
// before its signing secret can be saved here; it only echoes Slack's
|
|
2735
|
+
// own challenge back.
|
|
2736
|
+
async function handleSlackEvents(req: IncomingMessage, res: ServerResponse, agentName: string): Promise<void> {
|
|
2737
|
+
const rawBody = await readRawBody(req)
|
|
2738
|
+
const entry = getEntry(agentName)
|
|
2739
|
+
if (!entry) {
|
|
2740
|
+
res.writeHead(404, { 'content-type': 'text/plain' }).end('No such agent.')
|
|
2741
|
+
return
|
|
2742
|
+
}
|
|
2743
|
+
const event = parseSlackEvent(rawBody)
|
|
2744
|
+
if (event.type === 'url_verification') {
|
|
2745
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ challenge: event.challenge }))
|
|
2746
|
+
return
|
|
2747
|
+
}
|
|
2748
|
+
const { botToken, signingSecret } = slackCredentialsFor(entry.config)
|
|
2749
|
+
if (!signingSecret || !botToken) {
|
|
2750
|
+
res.writeHead(404, { 'content-type': 'text/plain' }).end('No Slack bot token and signing secret are set for this agent.')
|
|
2751
|
+
return
|
|
2752
|
+
}
|
|
2753
|
+
const timestamp = req.headers['x-slack-request-timestamp']
|
|
2754
|
+
const signature = req.headers['x-slack-signature']
|
|
2755
|
+
if (!verifySlackRequest(rawBody, typeof timestamp === 'string' ? timestamp : undefined, typeof signature === 'string' ? signature : undefined, signingSecret)) {
|
|
2756
|
+
res.writeHead(401, { 'content-type': 'text/plain' }).end('Invalid Slack signature.')
|
|
2757
|
+
return
|
|
2758
|
+
}
|
|
2759
|
+
// Slack wants an answer within three seconds; the reply is posted after.
|
|
2760
|
+
res.writeHead(200).end()
|
|
2761
|
+
if (event.type !== 'message' || !firstSighting(event)) return
|
|
2762
|
+
const environment = process.env.LOOPENGINE_ENV ?? 'production'
|
|
2763
|
+
await handleInboundMessage(event, {
|
|
2764
|
+
agentName,
|
|
2765
|
+
agentDir: agentDirFor(entry.config),
|
|
2766
|
+
systemPrompt: entry.config.systemPrompt,
|
|
2767
|
+
connector: createSlackConnector(botToken),
|
|
2768
|
+
createModelCall: () => entry.createModelCall(),
|
|
2769
|
+
getCheckpoint: (pendingId) => checkpoints.get(pendingId),
|
|
2770
|
+
getHistory: (checkpoint) => sessions.getHistory(`${checkpoint.tenant}:${environment}:${checkpoint.agent}:${checkpoint.sessionId}`),
|
|
2771
|
+
plan: process.env.LOOPENGINE_PLAN,
|
|
2772
|
+
})
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2708
2775
|
// Set once shutdown() starts, so /healthz turns 503 while the server
|
|
2709
2776
|
// drains and a router or load balancer stops sending it new requests.
|
|
2710
2777
|
let shuttingDown = false
|
|
@@ -2719,10 +2786,23 @@ const server = createServer(async (req, res) => {
|
|
|
2719
2786
|
return
|
|
2720
2787
|
}
|
|
2721
2788
|
|
|
2789
|
+
const slackEventsMatch = req.method === 'POST' && (req.url ?? '/').split('?')[0].match(/^\/hooks\/slack\/([^/]+)\/events$/)
|
|
2790
|
+
if (slackEventsMatch) {
|
|
2791
|
+
try {
|
|
2792
|
+
lastRequestAt = Date.now()
|
|
2793
|
+
await tracked(() => handleSlackEvents(req, res, decodeURIComponent(slackEventsMatch[1])))
|
|
2794
|
+
} catch (err) {
|
|
2795
|
+
console.error('[loopengine] Slack event failed:', err)
|
|
2796
|
+
if (!res.headersSent) res.writeHead(500).end()
|
|
2797
|
+
}
|
|
2798
|
+
return
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2722
2801
|
const slackHookMatch = req.method === 'POST' && (req.url ?? '/').split('?')[0].match(/^\/hooks\/slack\/([^/]+)$/)
|
|
2723
2802
|
if (slackHookMatch) {
|
|
2724
2803
|
try {
|
|
2725
|
-
|
|
2804
|
+
lastRequestAt = Date.now()
|
|
2805
|
+
await tracked(() => handleSlackInteraction(req, res, decodeURIComponent(slackHookMatch[1])))
|
|
2726
2806
|
} catch (err) {
|
|
2727
2807
|
console.error('[loopengine] Slack interaction failed:', err)
|
|
2728
2808
|
if (!res.headersSent) res.writeHead(500).end()
|
|
@@ -2733,7 +2813,8 @@ const server = createServer(async (req, res) => {
|
|
|
2733
2813
|
const decideMatch = (req.method === 'GET' || req.method === 'POST') && (req.url ?? '/').split('?')[0].match(/^\/decide\/([A-Za-z0-9_.-]+)$/)
|
|
2734
2814
|
if (decideMatch) {
|
|
2735
2815
|
try {
|
|
2736
|
-
|
|
2816
|
+
lastRequestAt = Date.now()
|
|
2817
|
+
await tracked(() => handleDecide(req, res, decideMatch[1]))
|
|
2737
2818
|
} catch (err) {
|
|
2738
2819
|
console.error('[loopengine] decision link failed:', err)
|
|
2739
2820
|
if (!res.headersSent) res.writeHead(500, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'Something went wrong; try again.' }))
|